Build a Telegram Bot That Talks to the Blockchain (Part 4)

In the previous part (part 1, part 2, part 3), we automated the invisible. Your bot could now check balances, record rewards, and notify you when something changed. It quietly worked in the background, doing its job.

But true magic happens when users can see and control what’s going on.

Typing commands like /subscribe and /subscriptions is functional, but it’s not intuitive.

So in this part, we’ll make the bot come alive.

You’ll be able to browse your wallets, edit settings, refresh data, and even delete subscriptions, all through inline menus and callback buttons.

No dashboards. No terminals. Just Telegram, transformed into your personal DeFi control panel.

Build a Telegram Bot That Talks to the Blockchain (Part 4)

Why Interaction Matters

When you think of Telegram bots, most people imagine something passive: text in, text out.

But in DeFi, your data is dynamic: rewards grow, balances change, thresholds trigger.

You need instant feedback.

That’s where inline keyboards come in.

They allow you to add buttons under each message, with live actions like Manage, Edit, Refresh, and Delete.

Each button sends a tiny JSON payload (callback_data), and the bot instantly updates the same message without flooding the chat.

It’s fast, elegant, and user-friendly - perfect for managing wallets and monitoring liquidity.

Step 1. Building the Subscription Overview

Let’s start with the /subscriptions command.

Instead of a static text list, it now shows your full portfolio, including live balances and rewards, joined directly from the database.

A single SQL query does the trick:

SELECT
  w.id, w.name, w.blockchain, w.token, w.wallet_address,
  r.balance_amount AS balance,
  r.reward_amount  AS rewards,
  r.created_at     AS last_update
FROM wallets w
LEFT JOIN (
  SELECT wallet_id, balance_amount, reward_amount, MAX(created_at) AS created_at
  FROM rewards
  GROUP BY wallet_id
) r ON w.id = r.wallet_id
WHERE w.user_id = :uid
ORDER BY w.id ASC;

And the output looks like this:

📋 *Your subscriptions (3)*

1) *Main Pool* [ETH/USDC]
   Balance: 2450 USDC
   Rewards: 8.4 USDC
   Address: 0xA12…B3f0

Below, we attach two simple buttons:

[🔄 Refresh] [⚙️ Manage]

So users can update data or move straight into the management panel, no typing required.

Step 2. Handling User Actions

When a button is pressed, Telegram doesn’t send a new message - it triggers a callback query.

The data from the button (like {"action":"wallet_details","wallet_id":12}) is sent to the bot and handled inside CallbackqueryCommand.php.

$decoded = json_decode($callback->getData(), true);

if ($decoded['action'] === 'wallet_details') {
    return $this->renderWalletDetails(
        $chat_id,
        $callback->getMessage()->getMessageId(),
        $decoded['wallet_id']
    );
}

The bot edits the same message using Request::editMessageText(), creating an illusion of an interactive app right inside Telegram.

Step 3. Wallet Details View

When a user clicks on a specific wallet, the bot fetches detailed info:

$sth = $pdo->prepare('SELECT * FROM wallets WHERE id = :id');
$sth->bindValue(':id', $wallet_id, PDO::PARAM_INT);
$sth->execute();
$wallet = $sth->fetch(PDO::FETCH_ASSOC);

And renders something like this:

📂 *Subscription Details*

Name: ETH–USDC Main  
Chain: Ethereum  
Token: USDC  
Address: 0xA12…B3f0  
Report Frequency: Daily  
Threshold: 5 USDC

With buttons:

[✏️ Edit] [🗑 Delete] [⬅ Back]

Now, users can tweak their subscription instantly or remove it entirely.

Step 4. Editing and Saving Changes

Editing uses the Conversation system to remember state.

When a user clicks Edit, the bot stores what field is being changed (name, threshold, or report_frequency) and waits for input.

Snippet from SubscriptionsCommand:

if (($notes['edit_step'] ?? '') === 'enter_value') {
    $new_value = $text;
    $wallet_id = (int)($notes['selected_wallet_id'] ?? 0);
    $field     = $notes['edit_field'] ?? null;

    if ($wallet_id && in_array($field, ['name','report_frequency','threshold'])) {
        $pdo = DB::getPdo();
        $sth = $pdo->prepare(
          "UPDATE wallets SET {$field} = :val WHERE id = :id AND user_id = :uid"
        );
        $sth->bindValue(':val', $new_value);
        $sth->bindValue(':id',  $wallet_id, PDO::PARAM_INT);
        $sth->bindValue(':uid', $user_id,   PDO::PARAM_INT);
        $sth->execute();
    }

    Request::sendMessage(['chat_id' => $chat_id, 'text' => 'Saved ✅']);
}

The user sees immediate feedback, and the wallet’s data is updated in-place.

Deleting works just as easily:

DELETE FROM wallets WHERE id = :id AND user_id = :uid;

and the UI refreshes to reflect the removal.

Step 5. Refresh and Back Navigation

To make it feel like a real dashboard, we added a Refresh button that reloads current balances and rewards, and a Back button that returns to the previous screen.

if ($decoded['action'] === 'refresh_manage') {
    return $this->renderManageList(
        $chat_id,
        $callback->getMessage()->getMessageId(),
        $user_id
    );
}

No new messages, no command spam, just a single editable view that always shows the latest data.

Step 6. A Seamless User Flow

Let’s walk through it:

  1. /subscriptions - list all wallets
  2. tap Manage - open interactive menu
  3. select a wallet - see live balances and rewards
  4. tap Edit - change threshold or name
  5. tap Back - return to the list
  6. tap Refresh - instantly reload data

That’s a complete management cycle — directly inside Telegram.

No dashboards, no external logins, no complexity — everything the user needs lives inside the chat window.

Why This Architecture Works

This design combines automation and interaction in a modular way:

  • /subscribe - adds new wallets
  • /subscriptions - displays current ones
  • CallbackqueryCommand - drives inline actions
  • MySQL - stores state and rewards
  • Cron - keeps data fresh in the background

It’s minimal, maintainable, and extensible - you can later add pagination, search filters, or even multi-chain grouping without rewriting the logic.

What You’ve Built

  • Inline wallet management (edit, delete, back, refresh)
  • Real-time balances and rewards in the subscriptions list
  • Persistent state through conversations
  • Smooth UX with minimal messages
  • Modular structure ready for automation

Your bot has officially evolved from a command-line assistant into a micro-dashboard.

It’s still running inside Telegram, but it behaves like a professional DeFi app.

file SubscriptionsCommand.php:

<?php

namespace Longman\TelegramBot\Commands\UserCommands;

use Longman\TelegramBot\Commands\UserCommand;
use Longman\TelegramBot\Request;
use Longman\TelegramBot\DB;
use Longman\TelegramBot\Conversation;
use Longman\TelegramBot\Entities\Keyboard;
use Longman\TelegramBot\Entities\InlineKeyboard;
use PDO;

class SubscriptionsCommand extends UserCommand
{
    protected $name = 'subscriptions';
    protected $description = 'Subscriptions list';
    protected $usage = '/subscriptions';
    protected $version = '1.0.0';
    protected $private_only = true;

    public function execute(): \Longman\TelegramBot\Entities\ServerResponse
    {

        $message = $this->getMessage();
        $chat_id = $message->getChat()->getId();
        $user_id = $message->getFrom()->getId();
        $text = trim($message->getText(true));

        $conv = new Conversation($user_id, $chat_id, $this->getName());
        $notes = &$conv->notes;
        !is_array($notes) && $notes = [];
        $state = (int)($notes['state'] ?? 0);

        switch ($state) {
            case 0:
                $notes['state'] = 1;
                $conv->update();
                return $this->renderSummaryNewMessage($chat_id, $user_id);
            case 1:
                if ($text === 'Refresh') {
                    return $this->renderSummaryNewMessage($chat_id, $user_id);
                }
                if ($text === 'Manage') {
                    $notes['state'] = 2;
                    $conv->update();
                    return $this->renderManageNewMessage($chat_id, $user_id);
                }
                return Request::sendMessage([
                    'chat_id' => $chat_id,
                    'text' => 'Choose: Refresh or Manage.',
                ]);

            case 2:
                return Request::sendMessage([
                    'chat_id' => $chat_id,
                    'text' => 'You are in interactive mode. Use buttons on screen.',
                ]);

            case 3:
                return Request::sendMessage([
                    'chat_id' => $chat_id,
                    'text' => 'You are in interactive mode. Use buttons on screen.',
                ]);

            case 4:
                if (($notes['edit_step'] ?? '') === 'choose_field') {
                    return Request::sendMessage([
                        'chat_id' => $chat_id,
                        'text' => 'You are in interactive mode. Use buttons on screen.',
                    ]);
                }
                if (($notes['edit_step'] ?? '') === 'enter_value') {
                    $new_value = $text;
                    $wallet_id = (int)($notes['selected_wallet_id'] ?? 0);
                    $field     = $notes['edit_field'] ?? null;

                    if ($wallet_id && in_array($field, ['name','report_frequency','threshold'], true)) {
                        $pdo = DB::getPdo();
                        $sql = "UPDATE wallets SET {$field} = :val WHERE id = :id AND user_id = :uid";
                        $sth = $pdo->prepare($sql);
                        $sth->bindValue(':val', $new_value);
                        $sth->bindValue(':id', $wallet_id, PDO::PARAM_INT);
                        $sth->bindValue(':uid', $user_id, PDO::PARAM_INT);
                        $sth->execute();
                    }

                    // go back to wallet details view
                    $notes['state'] = 2;
                    unset($notes['edit_step'], $notes['edit_field']);
                    $conv->update();

                    Request::sendMessage([
                        'chat_id' => $chat_id,
                        'text'    => 'Saved ✅',
                    ]);
                    return $this->renderManageNewMessage($chat_id, $user_id);
                }
                return Request::sendMessage([
                    'chat_id' => $chat_id,
                    'text' => 'You are in interactive mode. Use buttons on screen.',
                ]);

            default:
                $conv->stop();
                return Request::sendMessage([
                    'chat_id' => $chat_id,
                    'text' => 'Something went wrong. Try /subscriptions again.',
                ]);
        }
    }

    private function renderSummaryNewMessage(int $chat_id, int $user_id)
    {
        $text = $this->buildSummaryText($user_id);
        $kb = (new Keyboard(['Refresh', 'Manage']))->setResizeKeyboard(true)->setOneTimeKeyboard(false);

        return Request::sendMessage([
            'chat_id' => $chat_id,
            'text' => $text,
            'reply_markup' => $kb,
            'parse_mode' => 'Markdown',
        ]);
    }

    private function buildSummaryText(int $user_id): string
{
    $pdo = DB::getPdo();

    $sql = "
        SELECT
            w.id,
            w.name,
            w.blockchain,
            w.token,
            w.wallet_address,
            w.report_frequency,
            w.threshold,
            r.balance_amount   AS balance,
            r.reward_amount    AS rewards,
            r.created_at       AS last_update
        FROM wallets w
        LEFT JOIN (
            SELECT r1.wallet_id,
                   r1.balance_amount,
                   r1.reward_amount,
                   r1.created_at
            FROM rewards r1
            INNER JOIN (
                SELECT wallet_id, MAX(created_at) AS max_created
                FROM rewards
                GROUP BY wallet_id
            ) r2
                ON r1.wallet_id = r2.wallet_id
               AND r1.created_at = r2.max_created
        ) r
            ON w.id = r.wallet_id
        WHERE w.user_id = :uid
        ORDER BY w.id ASC
    ";

    $sth = $pdo->prepare($sql);
    $sth->bindValue(':uid', $user_id, PDO::PARAM_INT);
    $sth->execute();
    $rows = $sth->fetchAll(PDO::FETCH_ASSOC);

    if (!$rows) {
        return "You don't have any subscriptions yet. Use /subscribe to add one.";
    }

    $lines = ["📋 *Your subscriptions (" . count($rows) . ")*\n"];
    $i = 1;

    foreach ($rows as $r) {
        $short = $this->short($r['wallet_address']);
        $balance = $r['balance'] ?? '0';
        $rewards = $r['rewards'] ?? '0';

        $lines[] = sprintf(
            "%d) *%s* [%s/%s]\n".
            " Chain: %s \n".
            " Balance: %s %s\n".
            " Rewards: %s %s\n".
            " Address: `%s`\n",
            $i++,
            $r['name'],
            $r['blockchain'],
            $r['token'],
            $r['blockchain'],
            $balance,
            $r['token'],
            $rewards,
            $r['token'],
            $r['wallet_address']
        );
    }

    return implode("\n", $lines);
}

    private function renderManageNewMessage(int $chat_id, int $user_id)
    {
        [$text, $inline] = $this->buildManageUi($user_id);
        return Request::sendMessage([
            'chat_id' => $chat_id,
            'text' => $text,
            'reply_markup' => $inline,
        ]);
    }

    private function buildManageUi(int $user_id): array
    {
        $pdo = DB::getPdo();
        $rows = $pdo->query("SELECT id, blockchain, token, wallet_address FROM wallets WHERE user_id = " . (int)$user_id . " ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);

        if (!$rows) {
            return [
                "No subscriptions yet. Use /subscribe.",
                new InlineKeyboard([['text' => '⬅ Back', 'callback_data' => json_encode(["action" => "back", "to" => "list"])], ['text' => '🔄 Refresh', 'callback_data' => json_encode(["action" => "refresh_manage"])]]),
            ];
        }

        $text = "Manage your subscriptions. Pick one:";
        $grid = [];
        foreach ($rows as $r) {
            $label = sprintf('%s/%s %s', $r['blockchain'], $r['token'], $this->short($r['wallet_address']));
            $grid[] = [['text' => $label, 'callback_data' => json_encode(["action" => "wallet_details", "wallet_id" => $r['id']])]];
        }
        $grid[] = [['text' => '⬅ Back', 'callback_data' => json_encode(['action' => 'back', 'to' => 'list'])], ['text' => '🔄 Refresh', 'callback_data' => json_encode(['action' => 'refresh_manage'])]];

        return [$text, new InlineKeyboard(...$grid)];
    }

    private function short(string $addr): string
    {
        return strlen($addr) > 10 ? substr($addr, 0, 5) . '…' . substr($addr, -5) : $addr;
    }
}

file CallbackqueryCommand.php:

<?php
namespace Longman\TelegramBot\Commands\SystemCommands;

use Longman\TelegramBot\Commands\SystemCommand;
use Longman\TelegramBot\Request;
use Longman\TelegramBot\Conversation;
use Longman\TelegramBot\DB;
use Longman\TelegramBot\Entities\InlineKeyboard;
use PDO;

class CallbackqueryCommand extends SystemCommand
{
    protected $name = 'callbackquery';
    protected $description = 'Handle callback queries';
    protected $version = '1.0.0';

    public function execute(): \Longman\TelegramBot\Entities\ServerResponse
    {
        $callback = $this->getCallbackQuery();
        $data     = $callback->getData();
        $chat_id  = $callback->getMessage()->getChat()->getId();
        $user_id  = $callback->getFrom()->getId();

        $conv = new Conversation($user_id, $chat_id, 'subscriptions');
        $notes = &$conv->notes;
        !is_array($notes) && $notes = [];
        $state = (int)($notes['state'] ?? 0);

        $decoded = json_decode($data, true);
        if (!is_array($decoded)) {
            $decoded = [];
        }
        if (($decoded['action'] ?? '') === 'wallet_details') {
            $notes['selected_wallet_id'] = (int)$decoded['wallet_id'];
            $notes['state'] = 3;
            $conv->update();
            Request::answerCallbackQuery(['callback_query_id' => $callback->getId()]);
            return $this->renderWalletDetails($chat_id, $user_id, $callback->getMessage()->getMessageId(), $notes['selected_wallet_id']);
        }

        if (($decoded['action'] ?? '') === 'wallet_edit') {
            $notes['selected_wallet_id'] = (int)$decoded['wallet_id'];
            $notes['state'] = 4;
            $notes['edit_step'] = 'choose_field';
            $conv->update();
            Request::answerCallbackQuery(['callback_query_id' => $callback->getId()]);
            return $this->renderEditMenu($chat_id, $user_id, $callback->getMessage()->getMessageId(), $notes['selected_wallet_id']);
        }

        if (($decoded['action'] ?? '') === 'wallet_delete') {
            $this->deleteWallet($user_id, (int)$decoded['wallet_id']);
            $notes['state'] = 2;
            unset($notes['selected_wallet_id']);
            $conv->update();
            Request::answerCallbackQuery(['callback_query_id' => $callback->getId(), 'text' => 'Deleted']);
            return $this->renderManageList($chat_id, $callback->getMessage()->getMessageId(), $user_id);
        }

        if (($decoded['action'] ?? '') === 'refresh_manage') {
            $notes['state'] = 2;
            $conv->update();
            Request::answerCallbackQuery(['callback_query_id' => $callback->getId(), 'text' => 'Updated ✅']);
            return $this->renderManageList($chat_id, $callback->getMessage()->getMessageId(), $user_id);
        }

        if (($decoded['action'] ?? '') === 'edit_field') {
            $notes['selected_wallet_id'] = $decoded['wallet_id'];
            $notes['state'] = 4;
            $notes['edit_step'] = 'enter_value';
            $notes['edit_field'] = $decoded['field'] ?? null;
            $conv->update();

            Request::answerCallbackQuery([
                'callback_query_id' => $callback->getId(),
                'text'              => 'Send new value',
            ]);

            $prompt = "Enter new value for " . ($decoded['field'] ?? 'field') . ":";
            return Request::editMessageText([
                'chat_id'    => $chat_id,
                'message_id' => $callback->getMessage()->getMessageId(),
                'text'       => $prompt,
            ]);
        }

        if (($decoded['action'] ?? '') === 'back' && ($decoded['to'] ?? '') === 'manage') {
            $notes['state'] = 2;
            unset($notes['selected_wallet_id']);
            $conv->update();
            Request::answerCallbackQuery(['callback_query_id' => $callback->getId()]);
            return $this->renderManageList($chat_id, $callback->getMessage()->getMessageId(), $user_id);
        }

        if (($decoded['action'] ?? '') === 'back' && ($decoded['to'] ?? '') === 'list') {
            $notes['state'] = 1;
            unset($notes['selected_wallet_id']);
            $conv->update();
            Request::answerCallbackQuery(['callback_query_id' => $callback->getId()]);
            return $this->renderSummary($chat_id, $callback->getMessage()->getMessageId(), $user_id);
        }

        return Request::answerCallbackQuery(['callback_query_id' => $callback->getId()]);
    }


    private function renderSummary(int $chat_id, int $message_id, int $user_id)
    {
        $text = $this->buildSummaryText($user_id);
        return Request::editMessageText([
            'chat_id'    => $chat_id,
            'message_id' => $message_id,
            'text'       => $text,
            'parse_mode' => 'Markdown',
        ]);
    }

    private function buildSummaryText(int $user_id): string
{
    $pdo = DB::getPdo();

    $sql = "
        SELECT
            w.id,
            w.name,
            w.blockchain,
            w.token,
            w.wallet_address,
            w.report_frequency,
            w.threshold,
            r.balance_amount   AS balance,
            r.reward_amount    AS rewards,
            r.created_at       AS last_update
        FROM wallets w
        LEFT JOIN (
            SELECT r1.wallet_id,
                   r1.balance_amount,
                   r1.reward_amount,
                   r1.created_at
            FROM rewards r1
            INNER JOIN (
                SELECT wallet_id, MAX(created_at) AS max_created
                FROM rewards
                GROUP BY wallet_id
            ) r2
                ON r1.wallet_id = r2.wallet_id
               AND r1.created_at = r2.max_created
        ) r
            ON w.id = r.wallet_id
        WHERE w.user_id = :uid
        ORDER BY w.id ASC
    ";

    $sth = $pdo->prepare($sql);
    $sth->bindValue(':uid', $user_id, PDO::PARAM_INT);
    $sth->execute();
    $rows = $sth->fetchAll(PDO::FETCH_ASSOC);

    if (!$rows) {
        return "You don't have any subscriptions yet. Use /subscribe to add one.";
    }

    $lines = ["📋 *Your subscriptions (" . count($rows) . ")*\n"];
    $i = 1;
    foreach ($rows as $r) {
        $short   = strlen($r['wallet_address']) > 10
            ? substr($r['wallet_address'],0,5).'…'.substr($r['wallet_address'],-5)
            : $r['wallet_address'];

        $balance = $r['balance'] ?? '0';
        $rewards = $r['rewards'] ?? '0';

        $lines[] = sprintf(
            "%d) *%s* [%s/%s]\n".
            " Chain: %s \n".
            " Balance: %s %s\n".
            " Rewards: %s %s\n".
            " Address: `%s`\n",
            $i++,
            $r['name'],
            $r['blockchain'],
            $r['token'],
            $r['blockchain'],
            $balance,
            $r['token'],
            $rewards,
            $r['token'],
            $r['wallet_address']
        );
    }
    return implode("\n", $lines);
}

    private function renderManageList(int $chat_id, int $message_id, int $user_id)
    {
        [$text, $inline] = $this->buildManageUi($user_id);
        return Request::editMessageText([
            'chat_id'      => $chat_id,
            'message_id'   => $message_id,
            'text'         => $text,
            'reply_markup' => $inline,
        ]);
    }

    private function buildManageUi(int $user_id): array
    {
        $pdo = DB::getPdo();
        $rows = $pdo->query("SELECT * FROM wallets WHERE user_id = " . (int)$user_id . " ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);

        if (!$rows) {
            return [
                "No subscriptions yet. Use /subscribe.",
                new InlineKeyboard([['text' => '⬅ Back', 'callback_data' => json_encode(["action" => "back", "to" => "list"])]])
            ];
        }

        $text = "Manage your subscriptions. Pick one:";
        $grid = [];
        foreach ($rows as $r) {
            $short = strlen($r['wallet_address']) > 10 ? substr($r['wallet_address'],0,5).'…'.substr($r['wallet_address'],-5) : $r['wallet_address'];
            $label = sprintf('%s/%s %s', $r['blockchain'], $r['token'], $short);
            $grid[] = [['text' => $label, 'callback_data' => json_encode(["action" => "wallet_details", "wallet_id" => $r['id']])]];
        }
        $grid[] = [['text' => '⬅ Back', 'callback_data' => json_encode(['action' => 'back', 'to' => 'list'])], ['text' => '🔄 Refresh', 'callback_data' => json_encode(['action' => 'refresh_manage'])]];

        return [$text, new InlineKeyboard(...$grid)];
    }

    private function renderWalletDetails(int $chat_id, int $user_id, int $message_id, int $wallet_id)
{
    $pdo = DB::getPdo();

    $sql = "
        SELECT
            w.id,
            w.name,
            w.blockchain,
            w.token,
            w.wallet_address,
            w.report_frequency,
            w.threshold,
            r.balance_amount   AS balance,
            r.reward_amount    AS rewards,
            r.created_at       AS last_update
        FROM wallets w
        LEFT JOIN (
            SELECT r1.wallet_id,
                   r1.balance_amount,
                   r1.reward_amount,
                   r1.created_at
            FROM rewards r1
            INNER JOIN (
                SELECT wallet_id, MAX(created_at) AS max_created
                FROM rewards
                WHERE wallet_id = :wid
                GROUP BY wallet_id
            ) r2
                ON r1.wallet_id = r2.wallet_id
               AND r1.created_at = r2.max_created
        ) r
            ON w.id = r.wallet_id
        WHERE w.id = :wid
          AND w.user_id = :uid
        LIMIT 1
    ";

    $sth = $pdo->prepare($sql);
    $sth->bindValue(':wid', $wallet_id, PDO::PARAM_INT);
    $sth->bindValue(':uid', $user_id, PDO::PARAM_INT);
    $sth->execute();
    $w = $sth->fetch(PDO::FETCH_ASSOC);

    if (!$w) {
        return Request::editMessageText([
            'chat_id'    => $chat_id,
            'message_id' => $message_id,
            'text'       => 'Wallet not found.',
        ]);
    }

    $text  = "📂 *Subscription Details:*\n\n";
    $text .= "Name: " . $w['name'] . "\n";
    $text .= "Chain: " . $w['blockchain'] . "\n";
    $text .= "Token: " . $w['token'] . "\n";
    $text .= "Address: " . $w['wallet_address'] . "\n";
    $text .= "Balance: " . ($w['balance'] ?? '0') . " " . $w['token'] . "\n";
    $text .= "Rewards: " . ($w['rewards'] ?? '0') . " " . $w['token'] . "\n";
    $text .= "Report Frequency: " . $w['report_frequency'] . "\n";
    $text .= "Threshold: " . $w['threshold'] . "\n";
    if (!empty($w['last_update'])) {
        $text .= "Last update: " . $w['last_update'] . "\n";
    }

    $kbd = new InlineKeyboard(
        [['text' => '✏️ Edit',   'callback_data' => json_encode(["action" => "wallet_edit", "wallet_id" => $w['id']])]],
        [['text' => '🗑 Delete', 'callback_data' => json_encode(["action" => "wallet_delete", "wallet_id" => $w['id']])]],
        [['text' => '⬅ Back',    'callback_data' => json_encode(['action' => 'back', 'to' => 'manage'])]]
    );

    return Request::editMessageText([
        'chat_id'      => $chat_id,
        'message_id'   => $message_id,
        'text'         => $text,
        'reply_markup' => $kbd,
        'parse_mode'   => 'Markdown',
    ]);
}

    private function renderEditMenu(int $chat_id, int $user_id, int $message_id, int $wallet_id)
    {
        $pdo = DB::getPdo();
        $sth = $pdo->prepare("SELECT * FROM wallets WHERE id = :id and user_id = :uid");
        $sth->bindValue(':id', $wallet_id, PDO::PARAM_INT);
        $sth->bindValue(':uid', $user_id, PDO::PARAM_INT);
        $sth->execute();
        $w = $sth->fetch(PDO::FETCH_ASSOC);

        if (!$w) {
            return Request::editMessageText([
                'chat_id'    => $chat_id,
                'message_id' => $message_id,
                'text'       => 'Wallet not found.',
            ]);
        }

        $text = "✏️ *Edit subscription*\n\n";
        $text .= "Name: " . $w['name'] . "\n";
        $text .= "Report Frequency: " . $w['report_frequency'] . "\n";
        $text .= "Threshold: " . $w['threshold'] . "\n\n";
        $text .= "Select what you want to edit:";

        $kbd = new InlineKeyboard(
            [['text' => 'Name',              'callback_data' => json_encode(["action" => "edit_field", "field" => "name", "wallet_id" => $w['id']])]],
            [['text' => 'Report Frequency',  'callback_data' => json_encode(["action" => "edit_field", "field" => "report_frequency", "wallet_id" => $w['id']])]],
            [['text' => 'Threshold',         'callback_data' => json_encode(["action" => "edit_field", "field" => "threshold", "wallet_id" => $w['id']])]],
            [['text' => '⬅ Back',            'callback_data' => json_encode(['action' => 'back', 'to' => 'manage'])]]
        );

        return Request::editMessageText([
            'chat_id'      => $chat_id,
            'message_id'   => $message_id,
            'text'         => $text,
            'reply_markup' => $kbd,
            'parse_mode'   => 'Markdown',
        ]);
    }

    private function deleteWallet(int $user_id, int $id): void
    {
        $pdo = DB::getPdo();
        $sth = $pdo->prepare("DELETE FROM wallets WHERE id = :id AND user_id = :uid");
        $sth->bindValue(':id',  $id, PDO::PARAM_INT);
        $sth->bindValue(':uid', $user_id, PDO::PARAM_INT);
        $sth->execute();
    }
}

Bonus: The Welcome Experience

Every good product starts with a warm welcome — and your Telegram bot is no exception.

When a new user first writes /start, it’s the perfect moment to introduce what your bot does and what they can try next.

Here’s the code that handles that first impression:

<?php

namespace Longman\TelegramBot\Commands\SystemCommands;

use Longman\TelegramBot\Commands\SystemCommand;
use Longman\TelegramBot\Entities\ServerResponse;
use Longman\TelegramBot\Exception\TelegramException;

class StartCommand extends SystemCommand
{
    protected $name = 'start';
    protected $description = 'Start command';
    protected $usage = '/start';
    protected $version = '1.2.0';
    protected $private_only = true;

    public function execute(): ServerResponse
    {
        // Optional: handle deep-linking parameters if user came via a special link
        $deep_linking_parameter = $this->getMessage()->getText(true);

        return $this->replyToChat(
            "Hi! I'm your liquidity monitoring bot 👋\n\n".
            "Here’s what I can do for you:\n".
            "* /subscribe — add a wallet to monitor\n".
            "* /subscriptions — view & manage all your wallets\n\n".
            "I'll notify you when rewards cross your threshold 🚀",
            ['parse_mode' => 'Markdown']
        );
    }
}

It’s simple, but surprisingly powerful.

This short message immediately gives the user direction and confidence - no confusion, no guessing.

They instantly see that your bot is not just "another Telegram toy", but a real financial assistant that helps them track and manage liquidity positions.

Pro tip

If you want to make onboarding even smoother, add a deep-linking parameter like /start subscribe - it can take the user straight into the subscription flow, skipping extra steps.

Telegram supports this natively: https://core.telegram.org/api/links

Oleksii Opanasiuk Oleksii Opanasiuk

Oleksii Opanasiuk is a Lead Software Engineer and DevOps Specialist with 10+ years of experience in blockchain, cloud infrastructure, and full-stack development. He architects scalable systems, builds CI/CD pipelines, and helps teams deliver high-quality projects on time.

Leave a comment
Recent Comments (0)
No comments yet