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.

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…B3f0Below, 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 USDCWith 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:
/subscriptions- list all wallets- tap Manage - open interactive menu
- select a wallet - see live balances and rewards
- tap Edit - change threshold or name
- tap Back - return to the list
- 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




-0a4714b2-c0f8-4c92-a0ea-e47c5d800587.png)