Build a Quiz Bot with Dart and Televerse

Build a Telegram quiz bot with inline keyboards and callback queries in Dart.

Introduction

30 Second Summary

Quiz bots are everywhere in group chats. They break the ice, test knowledge, and keep conversations lively. Behind each one is a program listening for taps on buttons and responding in real time.

In this project, you will build a Telegram quiz bot from scratch using Dart and the Televerse framework. Your bot will ask trivia questions, let users tap inline buttons to answer, and announce their final score.

What You'll Build

A live Telegram quiz bot powered by Dart and Televerse that presents multiple-choice trivia questions through interactive inline keyboard buttons.

By the end of this project, you'll have:

  • A Dart console project with the Televerse framework configured and connected to the Telegram Bot API.
  • Type-safe command handlers for /start, /quiz, and /help that respond to users instantly.
  • Interactive inline keyboard buttons with callback query handling that tracks scores and advances through questions.
  • Secret Mission: Add a timed quiz mode where questions expire after 15 seconds.

Are there any prerequisites?

You need basic programming familiarity (variables, functions, if/else). No prior Dart or Telegram bot experience is required.

Before We Start

Before We Start...

Set Up Your Development Environment

Before you can build your quiz bot, you need three things on your machine: the Dart programming language, a code editor to write Dart in, and a Telegram bot identity that your code will control.

In this step, you will install the Dart SDK, set up VS Code with Dart support, and register a new bot with Telegram's BotFather.

In this step, get ready to:
  • Install the Dart SDK and verify it works.
  • Set up VS Code with the Dart extension.
  • Create a Telegram bot with BotFather and save the token.
Install the Dart SDK

The Dart SDK is the toolkit that lets you run Dart programs from the command line. It includes the dart command you will use throughout this project.

  • Open a terminal by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Check if Dart is already installed by running this command:
dart --version

✔️ I see version 3.5 or higher

You should see output like Dart SDK version: 3.11.6 (stable) or similar. As long as the version number starts with 3.5 or higher, you are ready to go.

Skip ahead to the next substep.

ⓧ I see an older version

You have Dart installed, but it is too old for this project. You need version 3.5 or higher. Upgrade using the instructions for your operating system below.

macOS

  • Upgrade Dart by running these commands:
brew tap dart-lang/dart
brew upgrade dart
  • After the upgrade finishes, run dart --version again to confirm you see 3.5 or higher.

Windows

  • Upgrade Dart by running this command in an elevated (Administrator) Command Prompt:
choco upgrade dart-sdk
  • After the upgrade finishes, close and reopen your terminal.
  • Run dart --version again to confirm you see 3.5 or higher.

ⓧ Command not found

Dart is not installed yet. Follow the instructions for your operating system below.

macOS

This requires Homebrew. If you do not have Homebrew, install it first from brew.sh.

  • Install the Dart SDK by running these commands:
brew tap dart-lang/dart
brew install dart
  • After the install finishes, run dart --version to confirm it works. You should see a version number like 3.11.6 or similar.

Windows

This requires Chocolatey. If you do not have Chocolatey, install it first from chocolatey.org/install.

  • Open an elevated (Administrator) Command Prompt.
  • Install the Dart SDK by running this command:
choco install dart-sdk
  • After the install finishes, close and reopen your terminal.
  • Run dart --version to confirm it works. You should see a version number like 3.11.6 or similar.

Still not seeing a version number?

Make sure your terminal session is fresh. Close the terminal completely and reopen it so your PATH updates take effect.

On macOS, if brew itself is not found, you need to install Homebrew first. On Windows, make sure you ran the install command in an Administrator prompt.

help me troubleshoot my Dart SDK installation

Install VS Code with the Dart extension

VS Code is a free code editor that gives you syntax highlighting, error detection, and code completion for Dart when paired with the official Dart extension.

  • Check if VS Code is already installed by pressing Cmd+Space (macOS) or the Windows key (Windows) and typing Visual Studio Code.

✔️ VS Code opens

VS Code is already installed. Skip ahead to installing the Dart extension below.

ⓧ I don't have VS Code

  • Download VS Code from code.visualstudio.com.
  • Run the installer and follow the prompts to complete the installation.
  • Once installed, open VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Visual Studio Code, and pressing Enter.

Now install the Dart extension to get language support in VS Code.

  • Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Type Dart in the search bar at the top of the panel.
  • Find the extension named Dart by the publisher Dart Code.
  • Click Install.

What does this extension do?

The Dart extension adds syntax highlighting, code completion, error checking, and debugging support for .dart files. Without it, VS Code treats your Dart code as plain text.

  • Confirm the extension is active by checking that Dart now appears in your list of installed extensions with a blue Disable button (meaning it is enabled).
Create a Telegram bot with BotFather

Every Telegram bot needs an identity registered through BotFather, Telegram's official bot-management bot. BotFather gives you a token that your code uses to authenticate with the Telegram Bot API.

  • Open Telegram on your phone or desktop. If you do not have Telegram, download it from telegram.org and create an account.
  • In the Telegram search bar, type @BotFather and open the chat with the verified BotFather account (it has a blue checkmark).
  • Send the message:
/newbot
  • BotFather will ask you for a display name. Type any name you like (for example, My Quiz Bot).
  • BotFather will then ask for a username. This must end in bot (for example, myquiz_2024_bot). Pick something unique.

After you confirm the username, BotFather replies with a message containing your bot token. It looks something like 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11.

  • Copy the token and save it somewhere safe (a text file or note-taking app). You will need it in the next step.

What is a bot token?

The token is like a password that identifies your bot to Telegram's servers. Anyone with this token can control your bot, so never share it publicly or commit it to a repository.

BotFather says the username is taken?

Bot usernames must be globally unique across all of Telegram. Try adding numbers or your initials to make it unique (for example, quizbot_jd_2024_bot).

help me pick a unique Telegram bot username

Your development environment is ready. You have the Dart SDK, a code editor with Dart support, and a registered Telegram bot. Next up, you will create your Dart project and add the Televerse framework.

Create Your Dart Project

Your environment is ready. Now it's time to create the actual project that will become your quiz bot.

Dart's built-in dart create command scaffolds a complete project structure for you. You'll create a console project, then add the Televerse framework as a dependency so your bot can communicate with Telegram.

In this step, get ready to:
  • Scaffold a Dart console project.
  • Add the Televerse framework dependency.
  • Install the dependency with dart pub get.
Scaffold a new Dart console project

The dart create command generates a ready-to-use project structure with a main file, dependency configuration, and test setup.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Scaffold a new Dart project called quiz_bot by running this command:
dart create quiz_bot

You should see output listing the files Dart created, ending with a message like Created project quiz_bot in quiz_bot!.

What did dart create just do?

The dart create command used the default console template to generate your project. It created a pubspec.yaml for managing dependencies, a bin/quiz_bot.dart file as your main entry point, and several other supporting files.

  • Move into the new project folder by running:
cd quiz_bot
  • Confirm the project files exist by running:
ls

You should see bin, lib, test, pubspec.yaml, and other files in the output.

Seeing 'command not found'?

Make sure the Dart SDK is installed and on your PATH. Try running dart --version to confirm. If that also fails, revisit the Dart installation from Step 1.

Help me fix my Dart installation

Now open the project in VS Code so you can edit your files.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the quiz_bot folder, then click Open.

Trust the authors?

VS Code may ask if you trust the authors of the files in this folder. Click Yes, I trust the authors to continue.

Add the Televerse dependency to pubspec.yaml

The pubspec.yaml file is where Dart projects declare their dependencies. You need to add Televerse here so your bot can use its classes and methods.

  • In the VS Code Explorer sidebar on the left, click pubspec.yaml to open it.
  • Select all the existing content and replace it with the following:
name: quiz_bot
description: A Telegram quiz bot built with Dart and Televerse.
version: 1.0.0

environment:
  sdk: ^3.5.0

dependencies:
  televerse: ^3.3.0

What does this file do?

The name and description fields identify your project. The environment section sets which Dart SDK versions are compatible.

The dependencies section lists packages your project needs. The televerse: ^3.3.0 line tells Dart to download version 3.3.0 or higher (but below 4.0.0) of the Televerse framework.

  • Save pubspec.yaml by pressing Cmd+S (macOS) or Ctrl+S (Windows).

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

Here's what your complete pubspec.yaml should look like:

name: quiz_bot
description: A Telegram quiz bot built with Dart and Televerse.
version: 1.0.0

environment:
  sdk: ^3.5.0

dependencies:
  televerse: ^3.3.0
Install the dependencies

With the dependency declared, you need to download it. The dart pub get command reads your pubspec.yaml and downloads all listed packages.

  • Open VS Code's integrated terminal by pressing Ctrl+` (backtick key, same shortcut on macOS and Windows).
  • Install the Televerse package by running:
dart pub get

You should see output that includes Resolving dependencies... followed by a list of downloaded packages. The final line will confirm how many dependencies were changed.

Seeing a version solving error?

Double check that your pubspec.yaml matches the code above exactly. YAML is sensitive to indentation. Each nested level must use exactly 2 spaces, not tabs.

Also confirm your Dart SDK version is 3.5.0 or higher by running dart --version.

Help me fix my dart pub get error

Your project is set up and Televerse is installed. Next up, you'll write the bot's first commands and see it respond in Telegram.

Build the Basic Bot with Commands

Your Dart project is scaffolded and Televerse is installed. Now it's time to write the bot logic that responds to user messages.

Every Telegram bot needs to listen for incoming messages and respond to commands. In this step, you'll register two command handlers that greet users and explain how the bot works.

In this step, get ready to:
  • Write the main bot file with /start and /help command handlers.
  • Configure the bot token and start the bot with long polling.
  • Test the bot responds to commands in Telegram.
Write the main bot file with /start and /help command handlers

When you scaffolded the project, Dart created a placeholder file at bin/quiz_bot.dart. You'll replace its contents with the bot logic that handles Telegram commands.

  • In VS Code, open bin/quiz_bot.dart from the Explorer sidebar on the left.
  • Select all the existing code with Cmd+A (macOS) or Ctrl+A (Windows) and delete it.
  • Paste the following code to create the full bot with both command handlers:
import 'dart:io';
import 'package:televerse/televerse.dart';

void main() async {
  final String token = Platform.environment['BOT_TOKEN']!;
  final bot = Bot(token);

  // /start command
  bot.command('start', (ctx) async {
    await ctx.reply(
      '👋 Welcome to Quiz Bot!\n\n'
      'I will test your knowledge with fun trivia questions.\n\n'
      'Commands:\n'
      '/quiz - Start a new quiz\n'
      '/help - Show help information',
    );
  });

  // /help command
  bot.command('help', (ctx) async {
    await ctx.reply(
      '📚 Quiz Bot Help\n\n'
      '/start - See the welcome message\n'
      '/quiz - Start a 3-question trivia quiz\n\n'
      'Tap the buttons to select your answer!',
    );
  });

  print('Bot is running...');
  await bot.start();
}

What does this code do?

  • Platform.environment['BOT_TOKEN']! reads your bot token from the system's environment variables. The ! tells Dart to crash immediately if the variable is missing, so you know right away something is wrong.
  • Bot(token) creates a new Televerse bot instance that connects to the Telegram Bot API.
  • bot.command('start', ...) registers a handler that fires whenever someone sends /start to your bot.
  • ctx.reply(...) sends a text message back to the user in the same chat.
  • bot.start() starts the bot with long polling, continuously checking Telegram for new messages.
  • Save bin/quiz_bot.dart.

Seeing red squiggles in VS Code?

  • Make sure you deleted all of the original scaffold code before pasting. Leftover lines cause duplicate main() errors.
  • Check that dart pub get completed successfully in the previous step. Without it, VS Code cannot find the Televerse package.

Why are there red squiggles in my quiz_bot.dart file?

✔️ Awesome, I've got everything!

Great. Double check you've saved the file before moving on.

ⓧ I'd like to double check the full code

Here is the complete bin/quiz_bot.dart file:

import 'dart:io';
import 'package:televerse/televerse.dart';

void main() async {
  final String token = Platform.environment['BOT_TOKEN']!;
  final bot = Bot(token);

  // /start command
  bot.command('start', (ctx) async {
    await ctx.reply(
      '👋 Welcome to Quiz Bot!\n\n'
      'I will test your knowledge with fun trivia questions.\n\n'
      'Commands:\n'
      '/quiz - Start a new quiz\n'
      '/help - Show help information',
    );
  });

  // /help command
  bot.command('help', (ctx) async {
    await ctx.reply(
      '📚 Quiz Bot Help\n\n'
      '/start - See the welcome message\n'
      '/quiz - Start a 3-question trivia quiz\n\n'
      'Tap the buttons to select your answer!',
    );
  });

  print('Bot is running...');
  await bot.start();
}
Configure the bot token and start the bot with long polling

Your code reads the bot token from an environment variable called BOT_TOKEN. You need to set this variable in your terminal before running the bot.

  • Switch back to your terminal inside the quiz_bot folder.
  • Set the BOT_TOKEN environment variable by running the command for your operating system:

macOS

This command also works on Linux.

export BOT_TOKEN=your-bot-token-here

Windows

$env:BOT_TOKEN="your-bot-token-here"
  • Replace your-bot-token-here with the token you received from BotFather in Step 1.
  • Start the bot by running:
dart run
  • You should see Bot is running... printed in your terminal.

What is long polling?

Long polling means your bot repeatedly asks Telegram "any new messages for me?" in a loop. This keeps the connection alive without needing a public web server or webhook URL.

It's the simplest way to run a bot during development. The bot.start() call handles all of this automatically.

Seeing an error about BOT_TOKEN?

  • If you see a null check error, the BOT_TOKEN variable was not set. Run the export (macOS/Linux) or $env: (Windows) command again in the same terminal window, then re-run dart run.
  • Make sure there are no spaces around the = sign in the export command.
  • If you get a 401 error, double check that you pasted the full token from BotFather correctly.

Help me fix my BOT_TOKEN environment variable error.

Test the bot responds to commands in Telegram

With the bot running in your terminal, you can now open Telegram and chat with it.

  • Open Telegram on your phone or desktop.
  • Search for your bot by the username you chose in BotFather (it starts with @).
  • Tap Start or type /start and send it.
  • You should see the welcome message listing the available commands.
  • Type /help and send it.
  • You should see the help message explaining how to use the bot.

Bot not responding?

  • Check your terminal. If you do not see Bot is running..., the bot crashed on startup. Read the error message and fix it.
  • Make sure you are messaging the correct bot username in Telegram.
  • If you restarted the terminal, you need to set BOT_TOKEN again and re-run dart run.

My Telegram bot is not responding to commands. Help me debug.

Your bot is alive and talking back. Next up, you'll create the quiz questions with inline keyboard buttons so users can tap their answers directly in the chat.

Create the Quiz with Inline Keyboards

Your bot can already greet users and show help text. Now it needs something to actually do.

In this step, you will define a set of trivia questions, build interactive buttons that appear directly in the chat, and wire up a /quiz command that kicks off the game.

In this step, get ready to:
  • Define quiz questions with answer choices and a correct answer index.
  • Build an InlineKeyboard for each question with callback data on every button.
  • Send the first question when the user runs /quiz.
Define the Question class

Every quiz question has three parts: the question text, a list of answer options, and the index of the correct option. A Dart class captures this structure cleanly.

  • In bin/quiz_bot.dart, find the line that says void main() async {.
  • Add the following code above it:
// Quiz data structure
class Question {
  final String text;
  final List<String> options;
  final int correctIndex;

  Question({
    required this.text,
    required this.options,
    required this.correctIndex,
  });
}

What does this code do?

The Question class is a simple data container with three fields:

  • text holds the question string displayed to the user.
  • options is the list of possible answers shown as buttons.
  • correctIndex stores which option (by position) is the right answer.

You won't see a change yet. The next chunk uses this class to define actual questions.

Add quiz questions and state tracking

With the class ready, you can define actual trivia questions. You also need two maps to track each user's score and which question they are currently on.

  • Directly below the Question class you just added (still above void main()), add the following code:
// Quiz questions
final List<Question> questions = [
  Question(
    text: '🌍 What is the largest planet in our solar system?',
    options: ['Earth', 'Jupiter', 'Saturn', 'Mars'],
    correctIndex: 1,
  ),
  Question(
    text: '🎵 Who painted the Mona Lisa?',
    options: ['Van Gogh', 'Picasso', 'Da Vinci', 'Monet'],
    correctIndex: 2,
  ),
  Question(
    text: '💻 What does "HTTP" stand for?',
    options: [
      'HyperText Transfer Protocol',
      'High Tech Transfer Process',
      'Hyper Transfer Text Protocol',
      'Home Tool Transfer Protocol',
    ],
    correctIndex: 0,
  ),
];

// Track user quiz state
final Map<int, int> userScores = {};
final Map<int, int> userCurrentQuestion = {};

What does this code do?

The questions list holds three Question objects. Each one stores answer options as a list of strings and points to the correct one by index (starting at 0).

The two maps below track per-user state. userScores counts correct answers for each chat. userCurrentQuestion remembers which question the user is on so the bot knows what to send next.

  • Save bin/quiz_bot.dart.
  • In your terminal, stop the bot if it is still running (press Ctrl+C), then restart it by running:
dart run bin/quiz_bot.dart

You should see Bot is running... with no errors. The bot does not have new behaviour yet, but a clean start confirms there are no typos in your data definitions.

Seeing a syntax error?

  • Make sure the Question class and questions list are placed above the void main() async { line, not inside it.
  • Check that every Question( entry ends with a closing ), and the list ends with ];.

Help me debug a Dart syntax error in my quiz_bot.dart file.

Build the inline keyboard for each question

Telegram's inline keyboards are rows of buttons that appear directly inside a chat message. When a user taps one, Telegram sends a callback query back to your bot with the button's data string. This is how the bot knows which answer was selected.

  • After the closing } of your main() function (at the very bottom of the file), add the following function:
Future<void> sendQuestion(Context ctx, int index) async {
  final question = questions[index];

  final inlineKeyboard = InlineKeyboard();
  for (var i = 0; i < question.options.length; i++) {
    inlineKeyboard.addButton(
      question.options[i],
      callbackData: 'answer_${index}_$i',
    );
    if (i < question.options.length - 1) {
      inlineKeyboard.row();
    }
  }

  await ctx.reply(
    'Question ${index + 1}/${questions.length}\n\n${question.text}',
    replyMarkup: inlineKeyboard,
  );
}

What does this code do?

sendQuestion takes the current Context and a question index. It builds an InlineKeyboard by looping through the question's options and calling addButton() for each one.

Each button's callbackData follows the pattern answer_{questionIndex}_{optionIndex}. This lets the bot parse exactly which question and which option the user picked when it receives the callback query later.

The .row() call starts a new row after each button, so every answer appears on its own line in the chat.

You won't see a change yet. The next chunk wires up the /quiz command that calls this function.

Wire up the /quiz command

The last piece connects everything. When a user sends /quiz, the bot resets their score, sets them to question 0, and sends the first question with its inline keyboard.

  • Inside bin/quiz_bot.dart, find the line print('Bot is running...'); inside main().
  • Add the following code directly above that line:
  // /quiz command
  bot.command('quiz', (ctx) async {
    final chatId = ctx.id;
    userScores[chatId.id] = 0;
    userCurrentQuestion[chatId.id] = 0;
    await sendQuestion(ctx, 0);
  });

What does this code do?

ctx.id returns the chat identifier. The handler uses it to reset userScores and userCurrentQuestion to 0, giving the user a fresh start each time they run /quiz.

It then calls sendQuestion(ctx, 0) to display the first question with its inline keyboard buttons.

  • Save bin/quiz_bot.dart.
  • Stop the bot (Ctrl+C) and restart it by running:
dart run bin/quiz_bot.dart
  • Switch to your Telegram chat with the bot.
  • Send /quiz.

You should see a message that says Question 1/3 followed by the first trivia question, with four answer buttons stacked vertically below it.

Don't see any buttons?

  • Make sure the sendQuestion() function is placed after the closing } of main(), not inside it.
  • Verify the /quiz handler is inside main(), above the print and bot.start() lines.
  • Check your terminal for error messages. A missing comma or bracket in the questions list is the most common cause.

Help me figure out why my /quiz command isn't showing inline keyboard buttons in Telegram.

Why InlineKeyboard instead of InlineMenu?

Televerse offers two approaches for interactive buttons. InlineMenu binds handlers directly to buttons, which is convenient for static menus. InlineKeyboard with bot.callbackQuery() gives you full control over callback data parsing and state management.

For a quiz, you need to track which question the callback belongs to and which option was selected. The callback data pattern (answer_0_2) makes that easy to parse. You will wire up the callback handler in the next step.

✔️ Awesome, I've got everything!

Great. Double check you have saved bin/quiz_bot.dart and that /quiz shows inline buttons in Telegram.

ⓧ I'd like to double check the full code

import 'dart:io';
import 'package:televerse/televerse.dart';

// Quiz data structure
class Question {
  final String text;
  final List<String> options;
  final int correctIndex;

  Question({
    required this.text,
    required this.options,
    required this.correctIndex,
  });
}

// Quiz questions
final List<Question> questions = [
  Question(
    text: '🌍 What is the largest planet in our solar system?',
    options: ['Earth', 'Jupiter', 'Saturn', 'Mars'],
    correctIndex: 1,
  ),
  Question(
    text: '🎵 Who painted the Mona Lisa?',
    options: ['Van Gogh', 'Picasso', 'Da Vinci', 'Monet'],
    correctIndex: 2,
  ),
  Question(
    text: '💻 What does "HTTP" stand for?',
    options: [
      'HyperText Transfer Protocol',
      'High Tech Transfer Process',
      'Hyper Transfer Text Protocol',
      'Home Tool Transfer Protocol',
    ],
    correctIndex: 0,
  ),
];

// Track user quiz state
final Map<int, int> userScores = {};
final Map<int, int> userCurrentQuestion = {};

void main() async {
  final String token = Platform.environment['BOT_TOKEN']!;
  final bot = Bot(token);

  // /start command
  bot.command('start', (ctx) async {
    await ctx.reply(
      '👋 Welcome to Quiz Bot!\n\n'
      'I will test your knowledge with fun trivia questions.\n\n'
      'Commands:\n'
      '/quiz - Start a new quiz\n'
      '/help - Show help information',
    );
  });

  // /help command
  bot.command('help', (ctx) async {
    await ctx.reply(
      '📚 Quiz Bot Help\n\n'
      '/start - See the welcome message\n'
      '/quiz - Start a 3-question trivia quiz\n\n'
      'Tap the buttons to select your answer!',
    );
  });

  // /quiz command
  bot.command('quiz', (ctx) async {
    final chatId = ctx.id;
    userScores[chatId.id] = 0;
    userCurrentQuestion[chatId.id] = 0;
    await sendQuestion(ctx, 0);
  });

  print('Bot is running...');
  await bot.start();
}

Future<void> sendQuestion(Context ctx, int index) async {
  final question = questions[index];

  final inlineKeyboard = InlineKeyboard();
  for (var i = 0; i < question.options.length; i++) {
    inlineKeyboard.addButton(
      question.options[i],
      callbackData: 'answer_${index}_$i',
    );
    if (i < question.options.length - 1) {
      inlineKeyboard.row();
    }
  }

  await ctx.reply(
    'Question ${index + 1}/${questions.length}\n\n${question.text}',
    replyMarkup: inlineKeyboard,
  );
}

Your bot now presents questions with tappable answer buttons. Next up, you will handle what happens when a user actually taps one of those buttons and track their score across all three questions.

Handle Answers and Track Score

Your quiz bot sends questions with clickable buttons, but nothing happens when a user taps an answer. The bot needs to listen for those button taps, figure out which answer was selected, and respond.

In this step, you will register a callback query handler that catches every button press, checks whether the answer is correct, tracks the score, and announces results at the end of the quiz.

In this step, get ready to:
  • Process callback queries when users tap answer buttons.
  • Track the score and advance to the next question.
  • Display the final score after all questions are answered.
Register the callback query handler

When a user taps an inline keyboard button, Telegram sends a callback query to your bot. The query contains the callbackData string you attached to each button (like answer_0_2). Your handler will parse that string to determine which question and which option was selected.

  • In bin/quiz_bot.dart, find the closing }); of the /quiz command handler.
  • Add the following callback query handler below it (before the print('Bot is running...'); line):
  // Callback query handler for quiz answers
  bot.callbackQuery(RegExp(r'^answer_\d+_\d+$'), (ctx) async {
    final data = ctx.callbackQuery?.data;
    if (data == null) return;

    final parts = data.split('_');
    final questionIndex = int.parse(parts[1]);
    final selectedIndex = int.parse(parts[2]);
    final chatId = ctx.id;

    // Check if this is the current question
    if (userCurrentQuestion[chatId.id] != questionIndex) {
      await ctx.answerCallbackQuery(text: 'This question has already been answered!');
      return;
    }

    final question = questions[questionIndex];
    final isCorrect = selectedIndex == question.correctIndex;

    if (isCorrect) {
      userScores[chatId.id] = (userScores[chatId.id] ?? 0) + 1;
      await ctx.answerCallbackQuery(text: '✅ Correct!');
    } else {
      await ctx.answerCallbackQuery(
        text: '❌ Wrong! The answer was: ${question.options[question.correctIndex]}',
      );
    }

    // Move to next question
    final nextIndex = questionIndex + 1;
    userCurrentQuestion[chatId.id] = nextIndex;

    if (nextIndex < questions.length) {
      await sendQuestion(ctx, nextIndex);
    } else {
      final score = userScores[chatId.id] ?? 0;
      final total = questions.length;
      String emoji;
      if (score == total) {
        emoji = '🏆';
      } else if (score >= total ~/ 2) {
        emoji = '👍';
      } else {
        emoji = '📖';
      }
      await ctx.reply(
        '$emoji Quiz Complete!\n\n'
        'Your score: $score/$total\n\n'
        'Run /quiz to try again!',
      );
    }
  });

What does this code do?

This handler fires whenever a user taps a button whose callback data matches the pattern answer_X_Y. Here is what each section does:

  • RegExp(r'^answer_\d+_\d+$') matches only callback data strings that follow your naming pattern. This prevents the handler from triggering on unrelated buttons.
  • data.split('_') breaks answer_0_2 into three parts so you can extract the question index and the selected option index.
  • The userCurrentQuestion check stops users from tapping old buttons after they have already moved on.
  • ctx.answerCallbackQuery() sends a toast notification to the user confirming whether they got it right or wrong.
  • After answering, the handler either sends the next question or displays the final score with an emoji based on performance.

What is a callback query?

A callback query is Telegram's way of telling your bot that a user pressed an inline keyboard button. It carries the callbackData string you assigned when you created the button.

Your bot must call answerCallbackQuery() to acknowledge it. Otherwise Telegram shows a loading spinner on the button indefinitely.

  • Save bin/quiz_bot.dart.

Seeing a syntax error?

Make sure the new handler sits between the closing }); of the /quiz handler and the print('Bot is running...'); line. Placing it inside another handler's closure causes nesting errors.

Also check that the RegExp string uses a raw string literal (r'^...'). Without the r prefix, backslashes are treated as escape characters.

Help me debug a syntax error in my Dart callback query handler

✔️ Awesome, I've got everything!

Great. Make sure you have saved your file before moving on.

ⓧ I'd like to double check the full code

import 'dart:io';
import 'package:televerse/televerse.dart';

// Quiz data structure
class Question {
  final String text;
  final List<String> options;
  final int correctIndex;

  Question({
    required this.text,
    required this.options,
    required this.correctIndex,
  });
}

// Quiz questions
final List<Question> questions = [
  Question(
    text: '🌍 What is the largest planet in our solar system?',
    options: ['Earth', 'Jupiter', 'Saturn', 'Mars'],
    correctIndex: 1,
  ),
  Question(
    text: '🎵 Who painted the Mona Lisa?',
    options: ['Van Gogh', 'Picasso', 'Da Vinci', 'Monet'],
    correctIndex: 2,
  ),
  Question(
    text: '💻 What does "HTTP" stand for?',
    options: [
      'HyperText Transfer Protocol',
      'High Tech Transfer Process',
      'Hyper Transfer Text Protocol',
      'Home Tool Transfer Protocol',
    ],
    correctIndex: 0,
  ),
];

// Track user quiz state
final Map<int, int> userScores = {};
final Map<int, int> userCurrentQuestion = {};

void main() async {
  final String token = Platform.environment['BOT_TOKEN']!;
  final bot = Bot(token);

  // /start command
  bot.command('start', (ctx) async {
    await ctx.reply(
      '👋 Welcome to Quiz Bot!\n\n'
      'I will test your knowledge with fun trivia questions.\n\n'
      'Commands:\n'
      '/quiz - Start a new quiz\n'
      '/help - Show help information',
    );
  });

  // /help command
  bot.command('help', (ctx) async {
    await ctx.reply(
      '📚 Quiz Bot Help\n\n'
      '/start - See the welcome message\n'
      '/quiz - Start a 3-question trivia quiz\n\n'
      'Tap the buttons to select your answer!',
    );
  });

  // /quiz command
  bot.command('quiz', (ctx) async {
    final chatId = ctx.id;
    userScores[chatId.id] = 0;
    userCurrentQuestion[chatId.id] = 0;
    await sendQuestion(ctx, 0);
  });

  // Callback query handler for quiz answers
  bot.callbackQuery(RegExp(r'^answer_\d+_\d+$'), (ctx) async {
    final data = ctx.callbackQuery?.data;
    if (data == null) return;

    final parts = data.split('_');
    final questionIndex = int.parse(parts[1]);
    final selectedIndex = int.parse(parts[2]);
    final chatId = ctx.id;

    // Check if this is the current question
    if (userCurrentQuestion[chatId.id] != questionIndex) {
      await ctx.answerCallbackQuery(text: 'This question has already been answered!');
      return;
    }

    final question = questions[questionIndex];
    final isCorrect = selectedIndex == question.correctIndex;

    if (isCorrect) {
      userScores[chatId.id] = (userScores[chatId.id] ?? 0) + 1;
      await ctx.answerCallbackQuery(text: '✅ Correct!');
    } else {
      await ctx.answerCallbackQuery(
        text: '❌ Wrong! The answer was: ${question.options[question.correctIndex]}',
      );
    }

    // Move to next question
    final nextIndex = questionIndex + 1;
    userCurrentQuestion[chatId.id] = nextIndex;

    if (nextIndex < questions.length) {
      await sendQuestion(ctx, nextIndex);
    } else {
      final score = userScores[chatId.id] ?? 0;
      final total = questions.length;
      String emoji;
      if (score == total) {
        emoji = '🏆';
      } else if (score >= total ~/ 2) {
        emoji = '👍';
      } else {
        emoji = '📖';
      }
      await ctx.reply(
        '$emoji Quiz Complete!\n\n'
        'Your score: $score/$total\n\n'
        'Run /quiz to try again!',
      );
    }
  });

  print('Bot is running...');
  await bot.start();
}

Future<void> sendQuestion(Context ctx, int index) async {
  final question = questions[index];

  final inlineKeyboard = InlineKeyboard();
  for (var i = 0; i < question.options.length; i++) {
    inlineKeyboard.addButton(
      question.options[i],
      callbackData: 'answer_${index}_$i',
    );
    if (i < question.options.length - 1) {
      inlineKeyboard.row();
    }
  }

  await ctx.reply(
    'Question ${index + 1}/${questions.length}\n\n${question.text}',
    replyMarkup: inlineKeyboard,
  );
}
Restart your bot and test score tracking

With the callback handler in place, your bot can now process button taps. Time to test that answers register correctly and the score increments.

  • Switch back to your terminal and stop the running bot with Ctrl+C.
  • Restart the bot by running:
dart run bin/quiz_bot.dart
  • Open your bot in Telegram and send /quiz.
  • Tap one of the answer buttons for the first question.
  • You should see a toast notification at the top of the chat showing either a correct or wrong message.
  • The next question should appear automatically after you answer.

Nothing happens when you tap a button?

Check your terminal for error messages. The most common cause is a mismatch between the callback data format in sendQuestion() and the RegExp pattern in bot.callbackQuery(). Both must use the answer_X_Y format.

If you started a quiz before adding the handler, try sending /quiz again to generate fresh buttons with the handler active.

Help me debug why my Telegram bot callback buttons are not responding

Complete the quiz and verify the final score

After answering all three questions, your bot should display a results message with your score and an emoji that reflects how well you did.

  • Answer the remaining questions by tapping the inline buttons.
  • After the third answer, you should see a message like "Quiz Complete!" with your score (for example, 2/3) and an emoji.
  • Try tapping an old button from a previous question. You should see a toast message saying the question has already been answered.
  • Send /quiz again to start a fresh round and confirm the score resets to zero.

How does the emoji scoring work?

  • Perfect score (3/3): trophy emoji.
  • Half or more correct (2/3): thumbs up emoji.
  • Less than half correct (0/3 or 1/3): book emoji, encouraging you to study up.

The ~/ operator is Dart's integer division. total ~/ 2 gives you the halfway point rounded down.

Your quiz bot is fully functional. Users can start a quiz, answer questions by tapping buttons, and see their results at the end. Next, you will tackle the Secret Mission and add a timed quiz mode with automatic question expiration.

Secret mission

Add a Timed Quiz Mode

Your quiz bot works great, but what if users want more pressure? Add a /timed command that gives users only 15 seconds per question. If time runs out, the question is marked wrong and the bot moves on automatically.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine and uses the free Telegram Bot API. There are no ongoing costs or cloud resources to worry about.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The quiz_bot project folder on your Desktop.
  • A Telegram bot registered with @BotFather.
  • The Dart SDK installed on your machine.

Keep everything running

No action needed. Choose this if you plan to keep building on your quiz bot or exploring Dart development.

  • Your bot only runs when you execute the Dart script. It does not consume resources when stopped.
  • The Dart SDK stays available for future projects.
  • Your Telegram bot token remains valid until you revoke it.

Pause - I'll come back to this later

Stop the running bot process to free up your terminal. Your project files stay on disk and are ready to pick up later.

  • Press Ctrl+C in the terminal where your bot is running to stop it.
  • Your quiz_bot folder and all its files remain on your Desktop.
  • To restart later, open a terminal, navigate to the project folder, set your BOT_TOKEN environment variable, and run dart run.

Delete - I don't want to use this again

Remove all project resources and start fresh. This cannot be undone.

  • Delete the project folder. On macOS/Linux, run:
rm -rf ~/Desktop/quiz_bot

On Windows, run:

rmdir /s /q "$env:USERPROFILE\Desktop\quiz_bot"
  • Delete your Telegram bot. Open your chat with @BotFather in Telegram and send /deletebot. Select your quiz bot from the list to permanently remove it.
  • Uninstall the Dart SDK. On macOS, run:
brew uninstall dart

On Windows, run:

choco uninstall dart-sdk
  • If you added BOT_TOKEN to your shell profile (.zshrc, .bashrc, or similar), open that file and remove the export line.

Nice Work!

Nice Work!

You just built a fully interactive Telegram quiz bot from scratch using Dart and the Televerse framework.

You've learned how to:

  • Connect to the Telegram Bot API using Televerse with type-safe command handlers for /start, /help, and /quiz.
  • Create interactive inline keyboard buttons with callback query handling that responds to user taps in real time.
  • Track user state (score and current question) across multiple interactions within a single quiz session.
  • Added a timed quiz mode with automatic 15-second question expiration using Future.delayed timers.

Ready to quiz yourself?