Tap-to-approve on Telegram without a second bot
The short answer: the final design sends one Telegram message with an approve and a deny button. Tapping either opens a tiny confirmation page against a private relay. A background job reads that relay every two minutes and records the decision, and the operator gets a confirmation message back in the same chat. No second bot, no conflicting connection type. A decision only counts if it carries the exact one-time reference for that specific card, arrives before it expires, and nobody has already answered it. Anything else, the system fails closed and nothing happens. Getting there took three attempts in one session. The first two both failed for the same underlying reason: a single messaging bot can only have one place its updates get delivered to.
Why this needed building at all
The system already had a module that could build an approval request and describe it. It just had nothing on the other end to read the operator's answer. It failed closed for want of a way to check for a response at all. The operator wanted a way to authorise things from his phone without typing, a tap, not a command. This is the story of what it took to give it one.
Attempt one: a dedicated approvals bot
The first attempt built a poller that repeatedly checks for a callback tap whose data matches the exact request it issued. It shipped with a full passing test suite and a real fail-closed design, a tap on the wrong card gets acknowledged and ignored, a tap from the wrong account gets rejected, and a tap that never arrives times out. It did not work in production. The main bot's taps already went to a live webhook, and a messaging platform will not let a bot both receive a webhook and be polled for updates at the same time, every polling call came back as a conflict. The fix attempted here was a second, dedicated bot used only for approval cards. That solved the conflict, but it meant a second bot token to create, store and keep alive just for this one feature, more moving parts than the job justified, a real dead end worth naming honestly.
Attempt two: zero setup, reply in the session
The second attempt skipped the platform's button mechanism altogether. Send the card on the existing main bot, give it a short reference code, and let the operator type the reference back in his own working session instead of tapping anything. A confirmation message was added so every decision, approve or deny, gets acknowledged back in the chat rather than silently recorded. A ledger underneath this recorded every decision with one deliberate guard, a decision can be logged under a real identity or under a self-test identity, and the check that gates a real action only ever returns approved for the real identity, so a self-test can rehearse the whole flow without ever being mistaken for a genuine authorisation. This worked, but it traded the tap that was actually wanted for typing a reference into a terminal session, workable, but not what was asked for.
The final design: URL buttons and a private relay
The design that actually delivers a tap-only flow on the main bot, with no second bot and no conflict, uses a plain URL button instead of a callback button. A URL button does not need to be polled at all, it just opens a link. Each card ships with an approve and a deny button that are URL buttons, both pointing at a private topic on a public relay service, with the decision and the card's one-time reference encoded in the link. Tapping either button opens a small confirmation page on the relay's side. Nothing more is needed on the operator's end.
A job reads that relay topic every two minutes, matches any message against the list of cards still pending, and checks three things before accepting a tap as a real decision: the reference has to match the exact card issued, the tap has to land before that card's expiry, and if two taps somehow arrive for the same card, the first one wins, later ones are ignored. A match writes the decision to the ledger and immediately sends a confirmation message in the same chat, quoting back what was just approved or denied. Cards nobody taps in time expire rather than default to approved. The default outcome of doing nothing is always no.
Why fail closed is the actual point
- A tap on a stale or wrong card is acknowledged and discarded, not silently accepted.
- A relay outage or a read error produces nothing recorded, never a default approval.
- The self-test decision path exists specifically so the whole flow can be exercised end to end without ever being able to pass the real check.
- An expired card defaults to not approved, not approved by timeout.
The interesting failure mode a system like this has to guard against is not that the button does not work, it is that something that looks like a tap gets treated as a real one. Three attempts and one honestly-named dead end later, the design that shipped treats every ambiguous case the same way: closed.
No bot token, chat identifier, relay topic name or webhook address appears anywhere in this post, or in anything published from this system. Those stay out of anything public.
AI-assistance disclosure: this post was drafted by Claude (Anthropic) directly from the operator's own commit history and source files, as part of documenting a real automation system he is building solo. The technical narrative above is reconstructed from commits and code, not from memory or invention.
If you want that same fail-closed discipline applied to your own business systems, message me for a free audit.
QUESTIONS ON RECORD
APPLY THIS TO YOUR OWN OPERATION
15 minutes is enough to know if there's a real gap worth closing.
GET YOUR FREE AI AUDIT: MESSAGE KYLE