Vibecode TeleMinute
track this build4 phases, 12 steps, beginner friendly0%A single personal bot with commands, keyword replies, and a basic broadcast is a solid weekend build on Telegram's friendly Bot API. What you are really buying is the hosted no-code layer: a dashboard a non-technical shop owner edits from a phone, scheduled broadcasts, lead-capture forms, analytics, and native iOS/Android apps. If you can run code you can replace your own subscription, but you cannot hand the DIY version to a client who cannot.
You are building a lean indie version of TeleMinute. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # TeleMinute · indie build 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. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node with a Telegram bot framework, one process, long polling | webhooks need a public certificate and this has to run on a spare machine or a small box | | Config | One YAML file of commands and keyword replies, validated on load and hot reloaded | the owner edits a file, and a typo in it must not take the bot down | | Store | SQLite beside the process with write-ahead logging on | chats, leads and the broadcast ledger, all of which you must be able to export or delete | | Broadcast | A materialised ledger with one row per recipient, drained under a token bucket | a loop over every chat is how a bot gets limited and how you lose track of who received what | | Ownership | One owner chat identifier, checked on every privileged command | an unchecked broadcast command is a stranger's megaphone | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the single bot process. - Get it: nodejs.org or a version manager. - Verify: node --version prints v22 or higher - [ ] **A bot token from the platform's bot registration account** · free - 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 - [ ] **An always-on machine** · a 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 - [ ] **Your own chat identifier** · free - 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 - [ ] **The platform's current published rate limits** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for TeleMinute, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · TeleMinute The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me one Telegram bot for one small business, with commands in a file, lead capture and a broadcast that stays inside the published rate limits to replace TeleMinute. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Node with a Telegram bot framework, one process, long polling · webhooks need a public certificate and this has to run on a spare machine or a small box - Config: One YAML file of commands and keyword replies, validated on load and hot reloaded · the owner edits a file, and a typo in it must not take the bot down - Store: SQLite beside the process with write-ahead logging on · chats, leads and the broadcast ledger, all of which you must be able to export or delete - Broadcast: A materialised ledger with one row per recipient, drained under a token bucket · a loop over every chat is how a bot gets limited and how you lose track of who received what - Ownership: One owner chat identifier, checked on every privileged command · an unchecked broadcast command is a stranger's megaphone ### Data model (create this before Phase 1) - 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. ### Phase 1 · One bot, one process, commands from a file Build: Read the token from the environment and start long polling. Load commands.yaml through a schema check, keep the last good configuration in memory, and hot reload on change. A failed load reports to the owner and keeps serving the previous configuration. Trap: 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. Done when: Starting a second instance against the same token exits with a clear message instead of polling, tested. A syntactically invalid commands.yaml leaves the previous configuration serving and sends the owner the parse error, tested by writing broken YAML while running. Every configuration load writes a row with the file hash and the outcome. The start command lists exactly the commands defined in the file, with no hard-coded list anywhere. Do not build yet: No lead capture, broadcasts, statistics or retention yet. ### Phase 2 · Conversations that survive a restart Build: Model the contact flow as a small state machine with the state stored per chat: name, then phone, then message, with validation, a cancel word and an expiry. Notify the owner when a lead completes. Resume from the stored step on the next message after a restart. Trap: 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. Done when: Restarting the process mid-conversation and sending the next message resumes at the correct step, tested by killing the process in the test. An abandoned conversation expires after the configured period and the next message starts fresh, tested by moving the clock. A cancel word exits the flow from any step and says so. Invalid input is re-prompted with a specific reason rather than dropping the person out of the flow. Do not build yet: No broadcast, statistics, export or deletion yet. ### Phase 3 · Broadcast inside the published limits Build: Creating a broadcast materialises one target row per chat before a single message is sent. A drainer sends under a token bucket set below the published rate, records each outcome, honours the retry interval the API returns on a limit error, and marks chats that have blocked the bot. Trap: 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. Done when: Killing the drainer halfway and restarting it sends to the remaining chats only, asserted against a stub that counts messages per chat. The send rate stays below the configured bucket rate, measured over a run of several hundred targets. A rate limit response causes a wait of at least the interval the API specified, tested with a stub returning one. A chat that has blocked the bot is marked and skipped by every later broadcast, tested. Do not build yet: No statistics, export, retention or supervision yet. ### Phase 4 · Statistics that are not a transcript Build: Count messages per day, unique chats, top commands and lead counts. Store a command name and a hash for ordinary messages and never the text. Keep customer words only in leads, which they deliberately sent, and give both tables a retention window. Trap: 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. Done when: No row in messages contains message text, asserted by a test that scans the column for a known phrase sent through the bot. The statistics command is owner-only and a non-owner receives the same response as for an unknown command, tested. Counts per day, unique chats and top commands are produced from indexed queries rather than a scan. Retention deletes message rows past the window and leaves the daily counts intact. Do not build yet: No export, deletion by person or supervision yet. ### Phase 5 · Leads, deletion and staying up Build: Export leads as CSV with the date range. Write one deletion function covering every table that holds a chat identifier and expose it as an owner command. Run under a supervisor with a restart policy, and post a short self-test to the owner on startup naming the config hash and version. Trap: 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. Done when: Deleting a person removes them from chats, messages, leads, conversations and broadcast targets, asserted by scanning every table for the identifier afterwards. Lead export produces a CSV with a date range and marks the rows exported. The process restarts automatically after a reboot and posts a startup message to the owner naming the version and the configuration hash. The README states that the hosted product's value is a dashboard a non-technical owner can use, which this build does not provide. Do not build yet: Nothing after this. ### Out of scope (and why) - A web dashboard a non-technical owner can edit from a phone, which is what the hosted product actually sells and is a far larger build than the bot. - Managing several bots from one process, because the token is the tenancy boundary here and mixing them makes every deletion question harder. - Scheduled and recurring broadcasts, which need a scheduler and a rethink of the ledger, and are worth adding only once one broadcast is reliable. - Buying an audience or messaging people who have not written to the bot first, which the platform does not permit and which this design has no table for. ### README must contain - How to create the bot and where the token and the owner chat identifier come from - That the bot cannot message anyone who has not messaged it first, so the broadcast audience is exactly the chats in the database - That broadcasts run under a token bucket set below the published rate limits, with the current figures and the date you checked them - That ordinary message text is never stored, that leads are, and what the retention windows are - That the hosted product's real value is a dashboard for someone who will not open a terminal, which this does not replace ===== AGENTS.md ===== # Agent instructions · TeleMinute indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node with a Telegram bot framework, one process, long polling, One YAML file of commands and keyword replies, validated on load and hot reloaded, SQLite beside the process with write-ahead logging on, A materialised ledger with one row per recipient, drained under a token bucket, One owner chat identifier, checked on every privileged command. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · TeleMinute 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · 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. ### Steps 1. Refuse to start twice A lock file or the database, but something. Files: `src/lock.js`, `tests/single-instance.test.js` 2. Validate the config and keep the last good one The owner is editing YAML on a phone at some point. Files: `src/config.js`, `tests/config-reload.test.js` 3. Generate the start message from the file A hard-coded command list drifts within a week. Files: `src/commands.js` ### Done when - [ ] A second instance refuses to start - [ ] Broken YAML keeps the previous config serving - [ ] Every config load is recorded with its hash - [ ] The start message comes from the file ### Watch 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. ## Phase 2 · Conversations that survive a restart Someone halfway through leaving their details still finishes after a deploy. ### Steps 1. Put the conversation step in the database The process is going to restart, probably at the worst moment. Files: `src/flows/contact.js`, `tests/resume.test.js` 2. Expire abandoned conversations Nobody should be stuck in a flow they forgot about. Files: `src/flows/expire.js` 3. Re-prompt on invalid input with the reason Dropping out of the flow loses the lead entirely. Files: `src/flows/contact.js` ### Done when - [ ] A restart mid-conversation resumes correctly - [ ] Abandoned conversations expire - [ ] A cancel word works from every step - [ ] Invalid input re-prompts with a reason ### Watch 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. ## Phase 3 · Broadcast inside the published limits A broadcast to every chat completes, is resumable and never sends twice. ### Steps 1. Materialise every target before sending anything The ledger is what makes a restart safe. Files: `src/broadcast/create.js`, `src/db/schema.sql` 2. Drain under a token bucket below the published rate Check the current published figures; they have changed before. Files: `src/broadcast/drain.js`, `tests/rate.test.js` 3. Mark blocked chats the first time the API says so Otherwise every future broadcast retries them. Files: `src/broadcast/drain.js` ### Done when - [ ] A restart mid-broadcast resumes without resending - [ ] The measured send rate stays under the bucket - [ ] A limit response is honoured for at least its interval - [ ] Blocked chats are marked and skipped afterwards ### Watch 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. ## Phase 4 · Statistics that are not a transcript The owner sees useful counts without the bot keeping a record of what customers typed. ### Steps 1. Store a hash, never the text You do not want a transcript of a cafe's customers. Files: `src/messages.js`, `tests/no-text-stored.test.js` 2. Make owner-only commands indistinguishable from unknown Do not confirm to a stranger that a command exists. Files: `src/auth.js` 3. Set a retention window on both tables from day one Adding it later means deleting data you already promised to keep. Files: `src/retention.js` ### Done when - [ ] No message text is stored anywhere - [ ] Owner-only commands look unknown to others - [ ] Statistics come from indexed queries - [ ] Retention runs and leaves the counts ### Watch 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. ## Not in this build - 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. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The bot's identity. Anyone with it is your bot, so treat it as a password. BOT_TOKEN=1234567890:AA...redacted # Required. The only chat that may broadcast, read statistics or delete a person. OWNER_CHAT_ID=123456789 # Required. Chats, leads, conversations and the broadcast ledger. Back this up. DATABASE_PATH=./data/bot.db # Required. Commands and keyword replies. Hot reloaded, validated, and never trusted blindly. CONFIG_PATH=./commands.yaml # Required. Set below the published limit. Check the current figure before changing it. BROADCAST_RATE_PER_SECOND=20 # Required. An abandoned contact flow expires and the next message starts fresh. CONVERSATION_EXPIRY_MINUTES=30 # Required. Message rows are deleted after this. The daily counts remain. MESSAGE_RETENTION_DAYS=90 # Required. Leads carry real personal details, so they get an end date too. LEAD_RETENTION_DAYS=365
You are building a lean indie version of TeleMinute. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # TeleMinute · indie build 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. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node with a Telegram bot framework, one process, long polling | webhooks need a public certificate and this has to run on a spare machine or a small box | | Config | One YAML file of commands and keyword replies, validated on load and hot reloaded | the owner edits a file, and a typo in it must not take the bot down | | Store | SQLite beside the process with write-ahead logging on | chats, leads and the broadcast ledger, all of which you must be able to export or delete | | Broadcast | A materialised ledger with one row per recipient, drained under a token bucket | a loop over every chat is how a bot gets limited and how you lose track of who received what | | Ownership | One owner chat identifier, checked on every privileged command | an unchecked broadcast command is a stranger's megaphone | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the single bot process. - Get it: nodejs.org or a version manager. - Verify: node --version prints v22 or higher - [ ] **A bot token from the platform's bot registration account** · free - 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 - [ ] **An always-on machine** · a 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 - [ ] **Your own chat identifier** · free - 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 - [ ] **The platform's current published rate limits** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for TeleMinute, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · TeleMinute The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me one Telegram bot for one small business, with commands in a file, lead capture and a broadcast that stays inside the published rate limits to replace TeleMinute. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Node with a Telegram bot framework, one process, long polling · webhooks need a public certificate and this has to run on a spare machine or a small box - Config: One YAML file of commands and keyword replies, validated on load and hot reloaded · the owner edits a file, and a typo in it must not take the bot down - Store: SQLite beside the process with write-ahead logging on · chats, leads and the broadcast ledger, all of which you must be able to export or delete - Broadcast: A materialised ledger with one row per recipient, drained under a token bucket · a loop over every chat is how a bot gets limited and how you lose track of who received what - Ownership: One owner chat identifier, checked on every privileged command · an unchecked broadcast command is a stranger's megaphone ### Data model (create this before Phase 1) - 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. ### Phase 1 · One bot, one process, commands from a file Build: Read the token from the environment and start long polling. Load commands.yaml through a schema check, keep the last good configuration in memory, and hot reload on change. A failed load reports to the owner and keeps serving the previous configuration. Trap: 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. Done when: Starting a second instance against the same token exits with a clear message instead of polling, tested. A syntactically invalid commands.yaml leaves the previous configuration serving and sends the owner the parse error, tested by writing broken YAML while running. Every configuration load writes a row with the file hash and the outcome. The start command lists exactly the commands defined in the file, with no hard-coded list anywhere. Do not build yet: No lead capture, broadcasts, statistics or retention yet. ### Phase 2 · Conversations that survive a restart Build: Model the contact flow as a small state machine with the state stored per chat: name, then phone, then message, with validation, a cancel word and an expiry. Notify the owner when a lead completes. Resume from the stored step on the next message after a restart. Trap: 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. Done when: Restarting the process mid-conversation and sending the next message resumes at the correct step, tested by killing the process in the test. An abandoned conversation expires after the configured period and the next message starts fresh, tested by moving the clock. A cancel word exits the flow from any step and says so. Invalid input is re-prompted with a specific reason rather than dropping the person out of the flow. Do not build yet: No broadcast, statistics, export or deletion yet. ### Phase 3 · Broadcast inside the published limits Build: Creating a broadcast materialises one target row per chat before a single message is sent. A drainer sends under a token bucket set below the published rate, records each outcome, honours the retry interval the API returns on a limit error, and marks chats that have blocked the bot. Trap: 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. Done when: Killing the drainer halfway and restarting it sends to the remaining chats only, asserted against a stub that counts messages per chat. The send rate stays below the configured bucket rate, measured over a run of several hundred targets. A rate limit response causes a wait of at least the interval the API specified, tested with a stub returning one. A chat that has blocked the bot is marked and skipped by every later broadcast, tested. Do not build yet: No statistics, export, retention or supervision yet. ### Phase 4 · Statistics that are not a transcript Build: Count messages per day, unique chats, top commands and lead counts. Store a command name and a hash for ordinary messages and never the text. Keep customer words only in leads, which they deliberately sent, and give both tables a retention window. Trap: 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. Done when: No row in messages contains message text, asserted by a test that scans the column for a known phrase sent through the bot. The statistics command is owner-only and a non-owner receives the same response as for an unknown command, tested. Counts per day, unique chats and top commands are produced from indexed queries rather than a scan. Retention deletes message rows past the window and leaves the daily counts intact. Do not build yet: No export, deletion by person or supervision yet. ### Phase 5 · Leads, deletion and staying up Build: Export leads as CSV with the date range. Write one deletion function covering every table that holds a chat identifier and expose it as an owner command. Run under a supervisor with a restart policy, and post a short self-test to the owner on startup naming the config hash and version. Trap: 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. Done when: Deleting a person removes them from chats, messages, leads, conversations and broadcast targets, asserted by scanning every table for the identifier afterwards. Lead export produces a CSV with a date range and marks the rows exported. The process restarts automatically after a reboot and posts a startup message to the owner naming the version and the configuration hash. The README states that the hosted product's value is a dashboard a non-technical owner can use, which this build does not provide. Do not build yet: Nothing after this. ### Out of scope (and why) - A web dashboard a non-technical owner can edit from a phone, which is what the hosted product actually sells and is a far larger build than the bot. - Managing several bots from one process, because the token is the tenancy boundary here and mixing them makes every deletion question harder. - Scheduled and recurring broadcasts, which need a scheduler and a rethink of the ledger, and are worth adding only once one broadcast is reliable. - Buying an audience or messaging people who have not written to the bot first, which the platform does not permit and which this design has no table for. ### README must contain - How to create the bot and where the token and the owner chat identifier come from - That the bot cannot message anyone who has not messaged it first, so the broadcast audience is exactly the chats in the database - That broadcasts run under a token bucket set below the published rate limits, with the current figures and the date you checked them - That ordinary message text is never stored, that leads are, and what the retention windows are - That the hosted product's real value is a dashboard for someone who will not open a terminal, which this does not replace ===== AGENTS.md ===== # Agent instructions · TeleMinute indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node with a Telegram bot framework, one process, long polling, One YAML file of commands and keyword replies, validated on load and hot reloaded, SQLite beside the process with write-ahead logging on, A materialised ledger with one row per recipient, drained under a token bucket, One owner chat identifier, checked on every privileged command. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · TeleMinute 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · 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. ### Steps 1. Refuse to start twice A lock file or the database, but something. Files: `src/lock.js`, `tests/single-instance.test.js` 2. Validate the config and keep the last good one The owner is editing YAML on a phone at some point. Files: `src/config.js`, `tests/config-reload.test.js` 3. Generate the start message from the file A hard-coded command list drifts within a week. Files: `src/commands.js` ### Done when - [ ] A second instance refuses to start - [ ] Broken YAML keeps the previous config serving - [ ] Every config load is recorded with its hash - [ ] The start message comes from the file ### Watch 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. ## Phase 2 · Conversations that survive a restart Someone halfway through leaving their details still finishes after a deploy. ### Steps 1. Put the conversation step in the database The process is going to restart, probably at the worst moment. Files: `src/flows/contact.js`, `tests/resume.test.js` 2. Expire abandoned conversations Nobody should be stuck in a flow they forgot about. Files: `src/flows/expire.js` 3. Re-prompt on invalid input with the reason Dropping out of the flow loses the lead entirely. Files: `src/flows/contact.js` ### Done when - [ ] A restart mid-conversation resumes correctly - [ ] Abandoned conversations expire - [ ] A cancel word works from every step - [ ] Invalid input re-prompts with a reason ### Watch 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. ## Phase 3 · Broadcast inside the published limits A broadcast to every chat completes, is resumable and never sends twice. ### Steps 1. Materialise every target before sending anything The ledger is what makes a restart safe. Files: `src/broadcast/create.js`, `src/db/schema.sql` 2. Drain under a token bucket below the published rate Check the current published figures; they have changed before. Files: `src/broadcast/drain.js`, `tests/rate.test.js` 3. Mark blocked chats the first time the API says so Otherwise every future broadcast retries them. Files: `src/broadcast/drain.js` ### Done when - [ ] A restart mid-broadcast resumes without resending - [ ] The measured send rate stays under the bucket - [ ] A limit response is honoured for at least its interval - [ ] Blocked chats are marked and skipped afterwards ### Watch 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. ## Phase 4 · Statistics that are not a transcript The owner sees useful counts without the bot keeping a record of what customers typed. ### Steps 1. Store a hash, never the text You do not want a transcript of a cafe's customers. Files: `src/messages.js`, `tests/no-text-stored.test.js` 2. Make owner-only commands indistinguishable from unknown Do not confirm to a stranger that a command exists. Files: `src/auth.js` 3. Set a retention window on both tables from day one Adding it later means deleting data you already promised to keep. Files: `src/retention.js` ### Done when - [ ] No message text is stored anywhere - [ ] Owner-only commands look unknown to others - [ ] Statistics come from indexed queries - [ ] Retention runs and leaves the counts ### Watch 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. ## Not in this build - 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. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The bot's identity. Anyone with it is your bot, so treat it as a password. BOT_TOKEN=1234567890:AA...redacted # Required. The only chat that may broadcast, read statistics or delete a person. OWNER_CHAT_ID=123456789 # Required. Chats, leads, conversations and the broadcast ledger. Back this up. DATABASE_PATH=./data/bot.db # Required. Commands and keyword replies. Hot reloaded, validated, and never trusted blindly. CONFIG_PATH=./commands.yaml # Required. Set below the published limit. Check the current figure before changing it. BROADCAST_RATE_PER_SECOND=20 # Required. An abandoned contact flow expires and the next message starts fresh. CONVERSATION_EXPIRY_MINUTES=30 # Required. Message rows are deleted after this. The daily counts remain. MESSAGE_RETENTION_DAYS=90 # Required. Leads carry real personal details, so they get an end date too. LEAD_RETENTION_DAYS=365
You are building a production product version of TeleMinute. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # TeleMinute · product brief ## Problem A single personal bot with commands, keyword replies, and a basic broadcast is a solid weekend build on Telegram's friendly Bot API. What you are really buying is the hosted no-code layer: a dashboard a non-technical shop owner edits from a phone, scheduled broadcasts, lead-capture forms, analytics, and native iOS/Android apps. If you can run code you can replace your own subscription, but you cannot hand the DIY version to a client who cannot. ## Product outcome One bot that a small business can actually rely on: commands from a file that a bad edit cannot take down, conversations that survive a deploy, a broadcast that resumes rather than resends, and a delete command that really deletes. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Telegram bot token - always-on process (VPS or spare machine) - SQLite - config file for commands ## Explicit non-goals for v1 - 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 ## Success criteria - A second instance is confirmed to refuse to start - A restart mid-conversation is confirmed to resume - A restart mid-broadcast is confirmed not to resend - No message text is confirmed stored anywhere - Deletion is confirmed to leave the identifier in no table ===== BRIEF.md ===== # Build brief · TeleMinute The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me one Telegram bot for one small business, with commands in a file, lead capture and a broadcast that stays inside the published rate limits to replace TeleMinute. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Node with a Telegram bot framework, one process, long polling · webhooks need a public certificate and this has to run on a spare machine or a small box - Config: One YAML file of commands and keyword replies, validated on load and hot reloaded · the owner edits a file, and a typo in it must not take the bot down - Store: SQLite beside the process with write-ahead logging on · chats, leads and the broadcast ledger, all of which you must be able to export or delete - Broadcast: A materialised ledger with one row per recipient, drained under a token bucket · a loop over every chat is how a bot gets limited and how you lose track of who received what - Ownership: One owner chat identifier, checked on every privileged command · an unchecked broadcast command is a stranger's megaphone ### Data model (create this before Phase 1) - 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. ### Phase 1 · One bot, one process, commands from a file Build: Read the token from the environment and start long polling. Load commands.yaml through a schema check, keep the last good configuration in memory, and hot reload on change. A failed load reports to the owner and keeps serving the previous configuration. Trap: 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. Done when: Starting a second instance against the same token exits with a clear message instead of polling, tested. A syntactically invalid commands.yaml leaves the previous configuration serving and sends the owner the parse error, tested by writing broken YAML while running. Every configuration load writes a row with the file hash and the outcome. The start command lists exactly the commands defined in the file, with no hard-coded list anywhere. Do not build yet: No lead capture, broadcasts, statistics or retention yet. ### Phase 2 · Conversations that survive a restart Build: Model the contact flow as a small state machine with the state stored per chat: name, then phone, then message, with validation, a cancel word and an expiry. Notify the owner when a lead completes. Resume from the stored step on the next message after a restart. Trap: 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. Done when: Restarting the process mid-conversation and sending the next message resumes at the correct step, tested by killing the process in the test. An abandoned conversation expires after the configured period and the next message starts fresh, tested by moving the clock. A cancel word exits the flow from any step and says so. Invalid input is re-prompted with a specific reason rather than dropping the person out of the flow. Do not build yet: No broadcast, statistics, export or deletion yet. ### Phase 3 · Broadcast inside the published limits Build: Creating a broadcast materialises one target row per chat before a single message is sent. A drainer sends under a token bucket set below the published rate, records each outcome, honours the retry interval the API returns on a limit error, and marks chats that have blocked the bot. Trap: 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. Done when: Killing the drainer halfway and restarting it sends to the remaining chats only, asserted against a stub that counts messages per chat. The send rate stays below the configured bucket rate, measured over a run of several hundred targets. A rate limit response causes a wait of at least the interval the API specified, tested with a stub returning one. A chat that has blocked the bot is marked and skipped by every later broadcast, tested. Do not build yet: No statistics, export, retention or supervision yet. ### Phase 4 · Statistics that are not a transcript Build: Count messages per day, unique chats, top commands and lead counts. Store a command name and a hash for ordinary messages and never the text. Keep customer words only in leads, which they deliberately sent, and give both tables a retention window. Trap: 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. Done when: No row in messages contains message text, asserted by a test that scans the column for a known phrase sent through the bot. The statistics command is owner-only and a non-owner receives the same response as for an unknown command, tested. Counts per day, unique chats and top commands are produced from indexed queries rather than a scan. Retention deletes message rows past the window and leaves the daily counts intact. Do not build yet: No export, deletion by person or supervision yet. ### Phase 5 · Leads, deletion and staying up Build: Export leads as CSV with the date range. Write one deletion function covering every table that holds a chat identifier and expose it as an owner command. Run under a supervisor with a restart policy, and post a short self-test to the owner on startup naming the config hash and version. Trap: 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. Done when: Deleting a person removes them from chats, messages, leads, conversations and broadcast targets, asserted by scanning every table for the identifier afterwards. Lead export produces a CSV with a date range and marks the rows exported. The process restarts automatically after a reboot and posts a startup message to the owner naming the version and the configuration hash. The README states that the hosted product's value is a dashboard a non-technical owner can use, which this build does not provide. Do not build yet: Nothing after this. ### Out of scope (and why) - A web dashboard a non-technical owner can edit from a phone, which is what the hosted product actually sells and is a far larger build than the bot. - Managing several bots from one process, because the token is the tenancy boundary here and mixing them makes every deletion question harder. - Scheduled and recurring broadcasts, which need a scheduler and a rethink of the ledger, and are worth adding only once one broadcast is reliable. - Buying an audience or messaging people who have not written to the bot first, which the platform does not permit and which this design has no table for. ### README must contain - How to create the bot and where the token and the owner chat identifier come from - That the bot cannot message anyone who has not messaged it first, so the broadcast audience is exactly the chats in the database - That broadcasts run under a token bucket set below the published rate limits, with the current figures and the date you checked them - That ordinary message text is never stored, that leads are, and what the retention windows are - That the hosted product's real value is a dashboard for someone who will not open a terminal, which this does not replace ===== ARCHITECTURE.md ===== # Architecture · TeleMinute ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node with a Telegram bot framework, one process, long polling | webhooks need a public certificate and this has to run on a spare machine or a small box | | Config | One YAML file of commands and keyword replies, validated on load and hot reloaded | the owner edits a file, and a typo in it must not take the bot down | | Store | SQLite beside the process with write-ahead logging on | chats, leads and the broadcast ledger, all of which you must be able to export or delete | | Broadcast | A materialised ledger with one row per recipient, drained under a token bucket | a loop over every chat is how a bot gets limited and how you lose track of who received what | | Ownership | One owner chat identifier, checked on every privileged command | an unchecked broadcast command is a stranger's megaphone | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Config | YAML loading, validation, hot reload, last good state. | Any format, provided a bad file never stops the bot. | | Flows | Conversation state machines stored per chat. | Any engine, provided state lives in the database. | | Broadcast | Target ledger, token bucket, blocked chats. | Not swappable for a loop, which is the whole trap. | | Stats | Counts, command tallies, indexed queries. | Any reporting, provided no message text is retained. | | Erase | One deletion path across every table. | Any implementation, provided the test asserts a clean database. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `BOT_TOKEN` · required, secret · The bot's identity. Anyone with it is your bot, so treat it as a password. - `OWNER_CHAT_ID` · required · The only chat that may broadcast, read statistics or delete a person. - `DATABASE_PATH` · required · Chats, leads, conversations and the broadcast ledger. Back this up. - `CONFIG_PATH` · required · Commands and keyword replies. Hot reloaded, validated, and never trusted blindly. - `BROADCAST_RATE_PER_SECOND` · required · Set below the published limit. Check the current figure before changing it. - `CONVERSATION_EXPIRY_MINUTES` · required · An abandoned contact flow expires and the next message starts fresh. - `MESSAGE_RETENTION_DAYS` · required · Message rows are deleted after this. The daily counts remain. - `LEAD_RETENTION_DAYS` · required · Leads carry real personal details, so they get an end date too. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · TeleMinute product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node with a Telegram bot framework, one process, long polling, One YAML file of commands and keyword replies, validated on load and hot reloaded, SQLite beside the process with write-ahead logging on, A materialised ledger with one row per recipient, drained under a token bucket, One owner chat identifier, checked on every privileged command. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - 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. ===== MILESTONES.md ===== # Delivery milestones · TeleMinute Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · 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. ### Steps 1. Refuse to start twice A lock file or the database, but something. Files: `src/lock.js`, `tests/single-instance.test.js` 2. Validate the config and keep the last good one The owner is editing YAML on a phone at some point. Files: `src/config.js`, `tests/config-reload.test.js` 3. Generate the start message from the file A hard-coded command list drifts within a week. Files: `src/commands.js` ### Done when - [ ] A second instance refuses to start - [ ] Broken YAML keeps the previous config serving - [ ] Every config load is recorded with its hash - [ ] The start message comes from the file ### Watch 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. ## M2 · Conversations that survive a restart Someone halfway through leaving their details still finishes after a deploy. ### Steps 1. Put the conversation step in the database The process is going to restart, probably at the worst moment. Files: `src/flows/contact.js`, `tests/resume.test.js` 2. Expire abandoned conversations Nobody should be stuck in a flow they forgot about. Files: `src/flows/expire.js` 3. Re-prompt on invalid input with the reason Dropping out of the flow loses the lead entirely. Files: `src/flows/contact.js` ### Done when - [ ] A restart mid-conversation resumes correctly - [ ] Abandoned conversations expire - [ ] A cancel word works from every step - [ ] Invalid input re-prompts with a reason ### Watch 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. ## M3 · Broadcast inside the published limits A broadcast to every chat completes, is resumable and never sends twice. ### Steps 1. Materialise every target before sending anything The ledger is what makes a restart safe. Files: `src/broadcast/create.js`, `src/db/schema.sql` 2. Drain under a token bucket below the published rate Check the current published figures; they have changed before. Files: `src/broadcast/drain.js`, `tests/rate.test.js` 3. Mark blocked chats the first time the API says so Otherwise every future broadcast retries them. Files: `src/broadcast/drain.js` ### Done when - [ ] A restart mid-broadcast resumes without resending - [ ] The measured send rate stays under the bucket - [ ] A limit response is honoured for at least its interval - [ ] Blocked chats are marked and skipped afterwards ### Watch 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. ## M4 · Statistics that are not a transcript The owner sees useful counts without the bot keeping a record of what customers typed. ### Steps 1. Store a hash, never the text You do not want a transcript of a cafe's customers. Files: `src/messages.js`, `tests/no-text-stored.test.js` 2. Make owner-only commands indistinguishable from unknown Do not confirm to a stranger that a command exists. Files: `src/auth.js` 3. Set a retention window on both tables from day one Adding it later means deleting data you already promised to keep. Files: `src/retention.js` ### Done when - [ ] No message text is stored anywhere - [ ] Owner-only commands look unknown to others - [ ] Statistics come from indexed queries - [ ] Retention runs and leaves the counts ### Watch 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. ## M5 · Leads, deletion and staying up (production only) Leads can leave the bot, a person can be removed completely, and the process comes back after a reboot. ### Steps 1. Write one deletion function for every table Then assert the identifier is gone from all of them. Files: `src/erase.js`, `tests/erase.test.js` 2. Export leads with a date range and mark them The owner wants them in a spreadsheet, not in your database. Files: `src/export.js` 3. Supervise the process and self-test on startup A bot that silently died on Tuesday is worse than no bot. Files: `ecosystem.config.js`, `src/selftest.js` ### Done when - [ ] Deletion leaves the identifier in no table - [ ] Lead export covers a date range and marks rows - [ ] The process restarts after a reboot - [ ] Startup posts a self-test naming version and config hash ### Watch 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. ===== OPERATIONS.md ===== # Operations · TeleMinute ## Backup The SQLite file nightly, offsite. It holds the leads, which are the only thing here you cannot recreate, and the broadcast ledger, which is the only record of who received what. ## Restore Restore the database, start with broadcasting disabled, confirm the chat count looks right, then enable. Restoring an old database and running a broadcast that was already partly sent is the one way this build can message people twice, so check the ledger states first. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring A startup message to the owner after every restart, the count of broadcast targets stuck in a pending state, the config load table for failed reloads, and the rate of blocked-chat markings, which climbing sharply usually means a broadcast people did not want. ## Incident checklist The two incidents are a silent outage and a duplicate broadcast. For the first, the startup self-test and the supervisor are the answer. For the second, the ledger tells you exactly who received it and how many times, which is why it is written before anything is sent. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A second instance is confirmed to refuse to start - [ ] A restart mid-conversation is confirmed to resume - [ ] A restart mid-broadcast is confirmed not to resend - [ ] No message text is confirmed stored anywhere - [ ] Deletion is confirmed to leave the identifier in no table ## Launch constraint Do not market omitted TeleMinute capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The bot's identity. Anyone with it is your bot, so treat it as a password. BOT_TOKEN=1234567890:AA...redacted # Required. The only chat that may broadcast, read statistics or delete a person. OWNER_CHAT_ID=123456789 # Required. Chats, leads, conversations and the broadcast ledger. Back this up. DATABASE_PATH=./data/bot.db # Required. Commands and keyword replies. Hot reloaded, validated, and never trusted blindly. CONFIG_PATH=./commands.yaml # Required. Set below the published limit. Check the current figure before changing it. BROADCAST_RATE_PER_SECOND=20 # Required. An abandoned contact flow expires and the next message starts fresh. CONVERSATION_EXPIRY_MINUTES=30 # Required. Message rows are deleted after this. The daily counts remain. MESSAGE_RETENTION_DAYS=90 # Required. Leads carry real personal details, so they get an end date too. LEAD_RETENTION_DAYS=365
# TeleMinute · indie build 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. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node with a Telegram bot framework, one process, long polling | webhooks need a public certificate and this has to run on a spare machine or a small box | | Config | One YAML file of commands and keyword replies, validated on load and hot reloaded | the owner edits a file, and a typo in it must not take the bot down | | Store | SQLite beside the process with write-ahead logging on | chats, leads and the broadcast ledger, all of which you must be able to export or delete | | Broadcast | A materialised ledger with one row per recipient, drained under a token bucket | a loop over every chat is how a bot gets limited and how you lose track of who received what | | Ownership | One owner chat identifier, checked on every privileged command | an unchecked broadcast command is a stranger's megaphone | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the single bot process. - Get it: nodejs.org or a version manager. - Verify: node --version prints v22 or higher - [ ] **A bot token from the platform's bot registration account** · free - 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 - [ ] **An always-on machine** · a 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 - [ ] **Your own chat identifier** · free - 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 - [ ] **The platform's current published rate limits** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for TeleMinute, and the README should say so rather than pretend.
# Build brief · TeleMinute The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me one Telegram bot for one small business, with commands in a file, lead capture and a broadcast that stays inside the published rate limits to replace TeleMinute. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Node with a Telegram bot framework, one process, long polling · webhooks need a public certificate and this has to run on a spare machine or a small box - Config: One YAML file of commands and keyword replies, validated on load and hot reloaded · the owner edits a file, and a typo in it must not take the bot down - Store: SQLite beside the process with write-ahead logging on · chats, leads and the broadcast ledger, all of which you must be able to export or delete - Broadcast: A materialised ledger with one row per recipient, drained under a token bucket · a loop over every chat is how a bot gets limited and how you lose track of who received what - Ownership: One owner chat identifier, checked on every privileged command · an unchecked broadcast command is a stranger's megaphone ### Data model (create this before Phase 1) - 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. ### Phase 1 · One bot, one process, commands from a file Build: Read the token from the environment and start long polling. Load commands.yaml through a schema check, keep the last good configuration in memory, and hot reload on change. A failed load reports to the owner and keeps serving the previous configuration. Trap: 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. Done when: Starting a second instance against the same token exits with a clear message instead of polling, tested. A syntactically invalid commands.yaml leaves the previous configuration serving and sends the owner the parse error, tested by writing broken YAML while running. Every configuration load writes a row with the file hash and the outcome. The start command lists exactly the commands defined in the file, with no hard-coded list anywhere. Do not build yet: No lead capture, broadcasts, statistics or retention yet. ### Phase 2 · Conversations that survive a restart Build: Model the contact flow as a small state machine with the state stored per chat: name, then phone, then message, with validation, a cancel word and an expiry. Notify the owner when a lead completes. Resume from the stored step on the next message after a restart. Trap: 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. Done when: Restarting the process mid-conversation and sending the next message resumes at the correct step, tested by killing the process in the test. An abandoned conversation expires after the configured period and the next message starts fresh, tested by moving the clock. A cancel word exits the flow from any step and says so. Invalid input is re-prompted with a specific reason rather than dropping the person out of the flow. Do not build yet: No broadcast, statistics, export or deletion yet. ### Phase 3 · Broadcast inside the published limits Build: Creating a broadcast materialises one target row per chat before a single message is sent. A drainer sends under a token bucket set below the published rate, records each outcome, honours the retry interval the API returns on a limit error, and marks chats that have blocked the bot. Trap: 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. Done when: Killing the drainer halfway and restarting it sends to the remaining chats only, asserted against a stub that counts messages per chat. The send rate stays below the configured bucket rate, measured over a run of several hundred targets. A rate limit response causes a wait of at least the interval the API specified, tested with a stub returning one. A chat that has blocked the bot is marked and skipped by every later broadcast, tested. Do not build yet: No statistics, export, retention or supervision yet. ### Phase 4 · Statistics that are not a transcript Build: Count messages per day, unique chats, top commands and lead counts. Store a command name and a hash for ordinary messages and never the text. Keep customer words only in leads, which they deliberately sent, and give both tables a retention window. Trap: 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. Done when: No row in messages contains message text, asserted by a test that scans the column for a known phrase sent through the bot. The statistics command is owner-only and a non-owner receives the same response as for an unknown command, tested. Counts per day, unique chats and top commands are produced from indexed queries rather than a scan. Retention deletes message rows past the window and leaves the daily counts intact. Do not build yet: No export, deletion by person or supervision yet. ### Phase 5 · Leads, deletion and staying up Build: Export leads as CSV with the date range. Write one deletion function covering every table that holds a chat identifier and expose it as an owner command. Run under a supervisor with a restart policy, and post a short self-test to the owner on startup naming the config hash and version. Trap: 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. Done when: Deleting a person removes them from chats, messages, leads, conversations and broadcast targets, asserted by scanning every table for the identifier afterwards. Lead export produces a CSV with a date range and marks the rows exported. The process restarts automatically after a reboot and posts a startup message to the owner naming the version and the configuration hash. The README states that the hosted product's value is a dashboard a non-technical owner can use, which this build does not provide. Do not build yet: Nothing after this. ### Out of scope (and why) - A web dashboard a non-technical owner can edit from a phone, which is what the hosted product actually sells and is a far larger build than the bot. - Managing several bots from one process, because the token is the tenancy boundary here and mixing them makes every deletion question harder. - Scheduled and recurring broadcasts, which need a scheduler and a rethink of the ledger, and are worth adding only once one broadcast is reliable. - Buying an audience or messaging people who have not written to the bot first, which the platform does not permit and which this design has no table for. ### README must contain - How to create the bot and where the token and the owner chat identifier come from - That the bot cannot message anyone who has not messaged it first, so the broadcast audience is exactly the chats in the database - That broadcasts run under a token bucket set below the published rate limits, with the current figures and the date you checked them - That ordinary message text is never stored, that leads are, and what the retention windows are - That the hosted product's real value is a dashboard for someone who will not open a terminal, which this does not replace
# Agent instructions · TeleMinute indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node with a Telegram bot framework, one process, long polling, One YAML file of commands and keyword replies, validated on load and hot reloaded, SQLite beside the process with write-ahead logging on, A materialised ledger with one row per recipient, drained under a token bucket, One owner chat identifier, checked on every privileged command. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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.
# Build plan · TeleMinute 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · 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. ### Steps 1. Refuse to start twice A lock file or the database, but something. Files: `src/lock.js`, `tests/single-instance.test.js` 2. Validate the config and keep the last good one The owner is editing YAML on a phone at some point. Files: `src/config.js`, `tests/config-reload.test.js` 3. Generate the start message from the file A hard-coded command list drifts within a week. Files: `src/commands.js` ### Done when - [ ] A second instance refuses to start - [ ] Broken YAML keeps the previous config serving - [ ] Every config load is recorded with its hash - [ ] The start message comes from the file ### Watch 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. ## Phase 2 · Conversations that survive a restart Someone halfway through leaving their details still finishes after a deploy. ### Steps 1. Put the conversation step in the database The process is going to restart, probably at the worst moment. Files: `src/flows/contact.js`, `tests/resume.test.js` 2. Expire abandoned conversations Nobody should be stuck in a flow they forgot about. Files: `src/flows/expire.js` 3. Re-prompt on invalid input with the reason Dropping out of the flow loses the lead entirely. Files: `src/flows/contact.js` ### Done when - [ ] A restart mid-conversation resumes correctly - [ ] Abandoned conversations expire - [ ] A cancel word works from every step - [ ] Invalid input re-prompts with a reason ### Watch 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. ## Phase 3 · Broadcast inside the published limits A broadcast to every chat completes, is resumable and never sends twice. ### Steps 1. Materialise every target before sending anything The ledger is what makes a restart safe. Files: `src/broadcast/create.js`, `src/db/schema.sql` 2. Drain under a token bucket below the published rate Check the current published figures; they have changed before. Files: `src/broadcast/drain.js`, `tests/rate.test.js` 3. Mark blocked chats the first time the API says so Otherwise every future broadcast retries them. Files: `src/broadcast/drain.js` ### Done when - [ ] A restart mid-broadcast resumes without resending - [ ] The measured send rate stays under the bucket - [ ] A limit response is honoured for at least its interval - [ ] Blocked chats are marked and skipped afterwards ### Watch 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. ## Phase 4 · Statistics that are not a transcript The owner sees useful counts without the bot keeping a record of what customers typed. ### Steps 1. Store a hash, never the text You do not want a transcript of a cafe's customers. Files: `src/messages.js`, `tests/no-text-stored.test.js` 2. Make owner-only commands indistinguishable from unknown Do not confirm to a stranger that a command exists. Files: `src/auth.js` 3. Set a retention window on both tables from day one Adding it later means deleting data you already promised to keep. Files: `src/retention.js` ### Done when - [ ] No message text is stored anywhere - [ ] Owner-only commands look unknown to others - [ ] Statistics come from indexed queries - [ ] Retention runs and leaves the counts ### Watch 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. ## Not in this build - 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. ## After v1, if you want it - 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
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The bot's identity. Anyone with it is your bot, so treat it as a password. BOT_TOKEN=1234567890:AA...redacted # Required. The only chat that may broadcast, read statistics or delete a person. OWNER_CHAT_ID=123456789 # Required. Chats, leads, conversations and the broadcast ledger. Back this up. DATABASE_PATH=./data/bot.db # Required. Commands and keyword replies. Hot reloaded, validated, and never trusted blindly. CONFIG_PATH=./commands.yaml # Required. Set below the published limit. Check the current figure before changing it. BROADCAST_RATE_PER_SECOND=20 # Required. An abandoned contact flow expires and the next message starts fresh. CONVERSATION_EXPIRY_MINUTES=30 # Required. Message rows are deleted after this. The daily counts remain. MESSAGE_RETENTION_DAYS=90 # Required. Leads carry real personal details, so they get an end date too. LEAD_RETENTION_DAYS=365
# TeleMinute · product brief ## Problem A single personal bot with commands, keyword replies, and a basic broadcast is a solid weekend build on Telegram's friendly Bot API. What you are really buying is the hosted no-code layer: a dashboard a non-technical shop owner edits from a phone, scheduled broadcasts, lead-capture forms, analytics, and native iOS/Android apps. If you can run code you can replace your own subscription, but you cannot hand the DIY version to a client who cannot. ## Product outcome One bot that a small business can actually rely on: commands from a file that a bad edit cannot take down, conversations that survive a deploy, a broadcast that resumes rather than resends, and a delete command that really deletes. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Telegram bot token - always-on process (VPS or spare machine) - SQLite - config file for commands ## Explicit non-goals for v1 - 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 ## Success criteria - A second instance is confirmed to refuse to start - A restart mid-conversation is confirmed to resume - A restart mid-broadcast is confirmed not to resend - No message text is confirmed stored anywhere - Deletion is confirmed to leave the identifier in no table
# Build brief · TeleMinute The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me one Telegram bot for one small business, with commands in a file, lead capture and a broadcast that stays inside the published rate limits to replace TeleMinute. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Node with a Telegram bot framework, one process, long polling · webhooks need a public certificate and this has to run on a spare machine or a small box - Config: One YAML file of commands and keyword replies, validated on load and hot reloaded · the owner edits a file, and a typo in it must not take the bot down - Store: SQLite beside the process with write-ahead logging on · chats, leads and the broadcast ledger, all of which you must be able to export or delete - Broadcast: A materialised ledger with one row per recipient, drained under a token bucket · a loop over every chat is how a bot gets limited and how you lose track of who received what - Ownership: One owner chat identifier, checked on every privileged command · an unchecked broadcast command is a stranger's megaphone ### Data model (create this before Phase 1) - 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. ### Phase 1 · One bot, one process, commands from a file Build: Read the token from the environment and start long polling. Load commands.yaml through a schema check, keep the last good configuration in memory, and hot reload on change. A failed load reports to the owner and keeps serving the previous configuration. Trap: 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. Done when: Starting a second instance against the same token exits with a clear message instead of polling, tested. A syntactically invalid commands.yaml leaves the previous configuration serving and sends the owner the parse error, tested by writing broken YAML while running. Every configuration load writes a row with the file hash and the outcome. The start command lists exactly the commands defined in the file, with no hard-coded list anywhere. Do not build yet: No lead capture, broadcasts, statistics or retention yet. ### Phase 2 · Conversations that survive a restart Build: Model the contact flow as a small state machine with the state stored per chat: name, then phone, then message, with validation, a cancel word and an expiry. Notify the owner when a lead completes. Resume from the stored step on the next message after a restart. Trap: 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. Done when: Restarting the process mid-conversation and sending the next message resumes at the correct step, tested by killing the process in the test. An abandoned conversation expires after the configured period and the next message starts fresh, tested by moving the clock. A cancel word exits the flow from any step and says so. Invalid input is re-prompted with a specific reason rather than dropping the person out of the flow. Do not build yet: No broadcast, statistics, export or deletion yet. ### Phase 3 · Broadcast inside the published limits Build: Creating a broadcast materialises one target row per chat before a single message is sent. A drainer sends under a token bucket set below the published rate, records each outcome, honours the retry interval the API returns on a limit error, and marks chats that have blocked the bot. Trap: 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. Done when: Killing the drainer halfway and restarting it sends to the remaining chats only, asserted against a stub that counts messages per chat. The send rate stays below the configured bucket rate, measured over a run of several hundred targets. A rate limit response causes a wait of at least the interval the API specified, tested with a stub returning one. A chat that has blocked the bot is marked and skipped by every later broadcast, tested. Do not build yet: No statistics, export, retention or supervision yet. ### Phase 4 · Statistics that are not a transcript Build: Count messages per day, unique chats, top commands and lead counts. Store a command name and a hash for ordinary messages and never the text. Keep customer words only in leads, which they deliberately sent, and give both tables a retention window. Trap: 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. Done when: No row in messages contains message text, asserted by a test that scans the column for a known phrase sent through the bot. The statistics command is owner-only and a non-owner receives the same response as for an unknown command, tested. Counts per day, unique chats and top commands are produced from indexed queries rather than a scan. Retention deletes message rows past the window and leaves the daily counts intact. Do not build yet: No export, deletion by person or supervision yet. ### Phase 5 · Leads, deletion and staying up Build: Export leads as CSV with the date range. Write one deletion function covering every table that holds a chat identifier and expose it as an owner command. Run under a supervisor with a restart policy, and post a short self-test to the owner on startup naming the config hash and version. Trap: 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. Done when: Deleting a person removes them from chats, messages, leads, conversations and broadcast targets, asserted by scanning every table for the identifier afterwards. Lead export produces a CSV with a date range and marks the rows exported. The process restarts automatically after a reboot and posts a startup message to the owner naming the version and the configuration hash. The README states that the hosted product's value is a dashboard a non-technical owner can use, which this build does not provide. Do not build yet: Nothing after this. ### Out of scope (and why) - A web dashboard a non-technical owner can edit from a phone, which is what the hosted product actually sells and is a far larger build than the bot. - Managing several bots from one process, because the token is the tenancy boundary here and mixing them makes every deletion question harder. - Scheduled and recurring broadcasts, which need a scheduler and a rethink of the ledger, and are worth adding only once one broadcast is reliable. - Buying an audience or messaging people who have not written to the bot first, which the platform does not permit and which this design has no table for. ### README must contain - How to create the bot and where the token and the owner chat identifier come from - That the bot cannot message anyone who has not messaged it first, so the broadcast audience is exactly the chats in the database - That broadcasts run under a token bucket set below the published rate limits, with the current figures and the date you checked them - That ordinary message text is never stored, that leads are, and what the retention windows are - That the hosted product's real value is a dashboard for someone who will not open a terminal, which this does not replace
# Architecture · TeleMinute ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node with a Telegram bot framework, one process, long polling | webhooks need a public certificate and this has to run on a spare machine or a small box | | Config | One YAML file of commands and keyword replies, validated on load and hot reloaded | the owner edits a file, and a typo in it must not take the bot down | | Store | SQLite beside the process with write-ahead logging on | chats, leads and the broadcast ledger, all of which you must be able to export or delete | | Broadcast | A materialised ledger with one row per recipient, drained under a token bucket | a loop over every chat is how a bot gets limited and how you lose track of who received what | | Ownership | One owner chat identifier, checked on every privileged command | an unchecked broadcast command is a stranger's megaphone | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Config | YAML loading, validation, hot reload, last good state. | Any format, provided a bad file never stops the bot. | | Flows | Conversation state machines stored per chat. | Any engine, provided state lives in the database. | | Broadcast | Target ledger, token bucket, blocked chats. | Not swappable for a loop, which is the whole trap. | | Stats | Counts, command tallies, indexed queries. | Any reporting, provided no message text is retained. | | Erase | One deletion path across every table. | Any implementation, provided the test asserts a clean database. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `BOT_TOKEN` · required, secret · The bot's identity. Anyone with it is your bot, so treat it as a password. - `OWNER_CHAT_ID` · required · The only chat that may broadcast, read statistics or delete a person. - `DATABASE_PATH` · required · Chats, leads, conversations and the broadcast ledger. Back this up. - `CONFIG_PATH` · required · Commands and keyword replies. Hot reloaded, validated, and never trusted blindly. - `BROADCAST_RATE_PER_SECOND` · required · Set below the published limit. Check the current figure before changing it. - `CONVERSATION_EXPIRY_MINUTES` · required · An abandoned contact flow expires and the next message starts fresh. - `MESSAGE_RETENTION_DAYS` · required · Message rows are deleted after this. The daily counts remain. - `LEAD_RETENTION_DAYS` · required · Leads carry real personal details, so they get an end date too. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · TeleMinute product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node with a Telegram bot framework, one process, long polling, One YAML file of commands and keyword replies, validated on load and hot reloaded, SQLite beside the process with write-ahead logging on, A materialised ledger with one row per recipient, drained under a token bucket, One owner chat identifier, checked on every privileged command. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - 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.
# Delivery milestones · TeleMinute Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · 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. ### Steps 1. Refuse to start twice A lock file or the database, but something. Files: `src/lock.js`, `tests/single-instance.test.js` 2. Validate the config and keep the last good one The owner is editing YAML on a phone at some point. Files: `src/config.js`, `tests/config-reload.test.js` 3. Generate the start message from the file A hard-coded command list drifts within a week. Files: `src/commands.js` ### Done when - [ ] A second instance refuses to start - [ ] Broken YAML keeps the previous config serving - [ ] Every config load is recorded with its hash - [ ] The start message comes from the file ### Watch 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. ## M2 · Conversations that survive a restart Someone halfway through leaving their details still finishes after a deploy. ### Steps 1. Put the conversation step in the database The process is going to restart, probably at the worst moment. Files: `src/flows/contact.js`, `tests/resume.test.js` 2. Expire abandoned conversations Nobody should be stuck in a flow they forgot about. Files: `src/flows/expire.js` 3. Re-prompt on invalid input with the reason Dropping out of the flow loses the lead entirely. Files: `src/flows/contact.js` ### Done when - [ ] A restart mid-conversation resumes correctly - [ ] Abandoned conversations expire - [ ] A cancel word works from every step - [ ] Invalid input re-prompts with a reason ### Watch 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. ## M3 · Broadcast inside the published limits A broadcast to every chat completes, is resumable and never sends twice. ### Steps 1. Materialise every target before sending anything The ledger is what makes a restart safe. Files: `src/broadcast/create.js`, `src/db/schema.sql` 2. Drain under a token bucket below the published rate Check the current published figures; they have changed before. Files: `src/broadcast/drain.js`, `tests/rate.test.js` 3. Mark blocked chats the first time the API says so Otherwise every future broadcast retries them. Files: `src/broadcast/drain.js` ### Done when - [ ] A restart mid-broadcast resumes without resending - [ ] The measured send rate stays under the bucket - [ ] A limit response is honoured for at least its interval - [ ] Blocked chats are marked and skipped afterwards ### Watch 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. ## M4 · Statistics that are not a transcript The owner sees useful counts without the bot keeping a record of what customers typed. ### Steps 1. Store a hash, never the text You do not want a transcript of a cafe's customers. Files: `src/messages.js`, `tests/no-text-stored.test.js` 2. Make owner-only commands indistinguishable from unknown Do not confirm to a stranger that a command exists. Files: `src/auth.js` 3. Set a retention window on both tables from day one Adding it later means deleting data you already promised to keep. Files: `src/retention.js` ### Done when - [ ] No message text is stored anywhere - [ ] Owner-only commands look unknown to others - [ ] Statistics come from indexed queries - [ ] Retention runs and leaves the counts ### Watch 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. ## M5 · Leads, deletion and staying up (production only) Leads can leave the bot, a person can be removed completely, and the process comes back after a reboot. ### Steps 1. Write one deletion function for every table Then assert the identifier is gone from all of them. Files: `src/erase.js`, `tests/erase.test.js` 2. Export leads with a date range and mark them The owner wants them in a spreadsheet, not in your database. Files: `src/export.js` 3. Supervise the process and self-test on startup A bot that silently died on Tuesday is worse than no bot. Files: `ecosystem.config.js`, `src/selftest.js` ### Done when - [ ] Deletion leaves the identifier in no table - [ ] Lead export covers a date range and marks rows - [ ] The process restarts after a reboot - [ ] Startup posts a self-test naming version and config hash ### Watch 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.
# Operations · TeleMinute ## Backup The SQLite file nightly, offsite. It holds the leads, which are the only thing here you cannot recreate, and the broadcast ledger, which is the only record of who received what. ## Restore Restore the database, start with broadcasting disabled, confirm the chat count looks right, then enable. Restoring an old database and running a broadcast that was already partly sent is the one way this build can message people twice, so check the ledger states first. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring A startup message to the owner after every restart, the count of broadcast targets stuck in a pending state, the config load table for failed reloads, and the rate of blocked-chat markings, which climbing sharply usually means a broadcast people did not want. ## Incident checklist The two incidents are a silent outage and a duplicate broadcast. For the first, the startup self-test and the supervisor are the answer. For the second, the ledger tells you exactly who received it and how many times, which is why it is written before anything is sent. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A second instance is confirmed to refuse to start - [ ] A restart mid-conversation is confirmed to resume - [ ] A restart mid-broadcast is confirmed not to resend - [ ] No message text is confirmed stored anywhere - [ ] Deletion is confirmed to leave the identifier in no table ## Launch constraint Do not market omitted TeleMinute capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The bot's identity. Anyone with it is your bot, so treat it as a password. BOT_TOKEN=1234567890:AA...redacted # Required. The only chat that may broadcast, read statistics or delete a person. OWNER_CHAT_ID=123456789 # Required. Chats, leads, conversations and the broadcast ledger. Back this up. DATABASE_PATH=./data/bot.db # Required. Commands and keyword replies. Hot reloaded, validated, and never trusted blindly. CONFIG_PATH=./commands.yaml # Required. Set below the published limit. Check the current figure before changing it. BROADCAST_RATE_PER_SECOND=20 # Required. An abandoned contact flow expires and the next message starts fresh. CONVERSATION_EXPIRY_MINUTES=30 # Required. Message rows are deleted after this. The daily counts remain. MESSAGE_RETENTION_DAYS=90 # Required. Leads carry real personal details, so they get an end date too. LEAD_RETENTION_DAYS=365
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because the target user is a cafe or gym owner who will never open a terminal, and TeleMinute gives them a working bot in minutes from a phone.
xno-code dashboard
xmobile apps
xscheduled broadcasts
xlead-capture form builder
xanalytics
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
TeleMinute pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| premium | $9.99/workspace | — | Unlimited bots, commands, keyword replies, users and traffic; up to 4 images per command |
free tierno free tier
billingweekly ($3.99), monthly ($9.99) or one-time lifetime ($49.99); no annual plan
hidden costsThe nominal subscription is flat with unlimited bots and traffic; payment-processing, Telegram advertising or third-party integrations may create external costs, but no platform overage schedule is published
verified 2026-08-14 · source ↗
Vibecode TeleMinute
Kinda. The core of TeleMinute is buildable in a weekend with the prompt on this page, but there are real gaps: no-code dashboard, mobile apps. Read the honest list above before committing.
How much does TeleMinute cost?
TeleMinute costs about $9.99/month (Premium, checked 2026-08-14), which is $119.88 per year.
What do I lose by replacing TeleMinute?
Honestly: no-code dashboard; mobile apps; scheduled broadcasts; lead-capture form builder; analytics. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to TeleMinute?
Yes: Activepieces (A visual automation product with Telegram triggers, replies, forms, and broadcasts; you assemble the bot rather than code it.) n8n Community Edition (A visual Telegram workflow engine that can receive commands, branch, collect answers, and broadcast; self-hosting is the price of free.) The prompt is for when you want it exactly your way.