Build TeleMinute
KINDAreplaces $9.99/mosaves $119.88/yrback to the verdict
A single-business Telegram bot whose hard problems are a broadcast that survives rate limits and restarts without resending, conversation state that outlives a deploy, and holding customer details you are then obliged to be able to delete.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the single bot process.
Get it nodejs.org or a version manager. open ↗
Verify
node --version prints v22 or higher - API keyfree
Why This is the bot's identity and its tenancy boundary, and anyone holding it can be your bot.
Get it Message the platform's bot registration account, create a bot and copy the token.
Verify
A getMe call returns your bot's username - have readya few pounds a month, or free on hardware you own
Why Long polling means the process has to be running for the bot to answer, and a laptop that sleeps is a bot that is offline during business hours.
Get it A small virtual machine, a single-board computer or a spare box that stays on.
Verify
The bot answers a command with your laptop closed - have readyfree
Why Every privileged command is gated on this one number, so getting it wrong means either you cannot broadcast or somebody else can.
Get it Message the bot once and read the chat identifier from the update.
Verify
A privileged command works for you and looks unknown to a second account - have readyfree
Why The broadcast design is a token bucket set below those figures, and they are the kind of number that changes and that nobody rechecks.
Get it Read the platform's own bot documentation and note the figures with the date.
Verify
The README carries the figures, the source and the date checked
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- chats(chat_id PRIMARY KEY, kind, first_seen_at, last_seen_at, language_code, blocked_at) · blocked_at is set the moment the API tells you the user blocked the bot - messages(id, chat_id, at, kind, command, text_hash) · a hash, because you do not need to keep what customers typed - leads(id, chat_id, name, phone, message, at, exported_at, deleted_at) · the only place a customer's own words are kept, and they knowingly sent them - conversations(chat_id PRIMARY KEY, flow, step, collected_json, started_at, expires_at) · in the database, because the process will restart mid-conversation - broadcasts(id, at, body, created_by, state) and broadcast_targets(broadcast_id, chat_id, state, attempts, error_code, sent_at) · unique on the pair, so a restart resumes rather than resends - config_loads(id, at, file_hash, ok, error_text) · the owner will edit the file and want to know why nothing changed - There is no table of people the bot has not met. A Telegram bot cannot start a conversation, so your broadcast audience is exactly the set of chats that messaged you first, and that is the honest ceiling on this whole feature.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
BOT_TOKENsecret | required | 1234567890:AA...redacted | The bot's identity. Anyone with it is your bot, so treat it as a password. |
OWNER_CHAT_ID | required | 123456789 | The only chat that may broadcast, read statistics or delete a person. |
DATABASE_PATH | required | ./data/bot.db | Chats, leads, conversations and the broadcast ledger. Back this up. |
CONFIG_PATH | required | ./commands.yaml | Commands and keyword replies. Hot reloaded, validated, and never trusted blindly. |
BROADCAST_RATE_PER_SECOND | required | 20 | Set below the published limit. Check the current figure before changing it. |
CONVERSATION_EXPIRY_MINUTES | required | 30 | An abandoned contact flow expires and the next message starts fresh. |
MESSAGE_RETENTION_DAYS | required | 90 | Message rows are deleted after this. The daily counts remain. |
LEAD_RETENTION_DAYS | required | 365 | Leads carry real personal details, so they get an end date too. |
The build, in order
One bot, one process, commands from a file
The bot answers commands defined in a YAML file and a bad edit to that file never takes it offline.
A lock file or the database, but something.
Files
src/lock.jstests/single-instance.test.jsThe owner is editing YAML on a phone at some point.
Files
src/config.jstests/config-reload.test.jsA hard-coded command list drifts within a week.
Files
src/commands.js
done when · tick each as it passeswatch out- Two copies of the process polling the same token fight over updates and the platform rejects one of them with a conflict, which from the outside looks like a bot that answers every other message and sends people away thinking the business is broken; take an exclusive lock on the database file or a lock file at startup, refuse to start a second instance with a clear message, and check what your process supervisor does on a restart before you trust it not to overlap them.
Conversations that survive a restart
Someone halfway through leaving their details still finishes after a deploy.
The process is going to restart, probably at the worst moment.
Files
src/flows/contact.jstests/resume.test.jsNobody should be stuck in a flow they forgot about.
Files
src/flows/expire.jsDropping out of the flow loses the lead entirely.
Files
src/flows/contact.js
done when · tick each as it passeswatch out- Holding conversation state in a map in memory means a restart drops three people mid-flow, and the next thing they type, which is their phone number, gets answered as if it were a new message and possibly matched against a keyword reply; store the step and what has been collected in the database with an expiry, resume on the next message, and expire abandoned conversations rather than leaving somebody stuck in a flow forever.
Broadcast inside the published limits
A broadcast to every chat completes, is resumable and never sends twice.
The ledger is what makes a restart safe.
Files
src/broadcast/create.jssrc/db/schema.sqlCheck the current published figures; they have changed before.
Files
src/broadcast/drain.jstests/rate.test.jsOtherwise every future broadcast retries them.
Files
src/broadcast/drain.js
done when · tick each as it passeswatch out- Looping over every chat and sending as fast as the library will go gets you limited, because the platform's own bot guidance says a bot cannot broadcast more than about thirty messages a second and no more than twenty messages a minute into the same group, and the failures arrive partway through so the first few hundred people get the message and the rest silently do not; materialise the targets first, drain under a token bucket below the published figure, honour the retry interval the API hands back, and let a restart resume from the ledger instead of starting again.
Statistics that are not a transcript
The owner sees useful counts without the bot keeping a record of what customers typed.
You do not want a transcript of a cafe's customers.
Files
src/messages.jstests/no-text-stored.test.jsDo not confirm to a stranger that a command exists.
Files
src/auth.jsAdding it later means deleting data you already promised to keep.
Files
src/retention.js
done when · tick each as it passeswatch out- Keeping every incoming message so you can add analytics later means the shop's bot is now holding a transcript of what its customers typed, which is a disclosure problem you did not need and a deletion obligation you did not plan for; store the command name and a hash for ordinary traffic, keep real text only where the person was explicitly giving it to you, and set a retention period on both from the first release rather than after the first request.
Leads, deletion and staying upproduct builder
Leads can leave the bot, a person can be removed completely, and the process comes back after a reboot.
Then assert the identifier is gone from all of them.
Files
src/erase.jstests/erase.test.jsThe owner wants them in a spreadsheet, not in your database.
Files
src/export.jsA bot that silently died on Tuesday is worse than no bot.
Files
ecosystem.config.jssrc/selftest.js
done when · tick each as it passeswatch out- A delete command that removes the lead row and leaves the chat, the message counts and the broadcast ledger untouched has not deleted that person, and you will believe it did until somebody checks; write one function that takes a chat identifier and covers every table holding one, and prove it with a test that inserts a person into all of them, deletes, and then asserts the identifier appears nowhere in the database.
That is the whole plan for TeleMinute. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A web dashboard for a non-technical owner.
- Managing several bots from one process.
- Scheduled and recurring broadcasts.
- Messaging people who have not written to the bot.
- no-code dashboard
- mobile apps
- scheduled broadcasts
- lead-capture form builder
- analytics
- Opening hours in the config, so the bot answers differently when the shop is shut
- A weekly lead digest to the owner, which is the report they will actually read
- Media in command replies, once you have decided where those files live and how they are backed up
Need the files? The project pack on the verdict page hands your agent the whole brief · more no-code apps.