From e43b812f973e3069d0699781e3f53334db0bec80 Mon Sep 17 00:00:00 2001 From: martbost Date: Fri, 18 Sep 2026 09:37:10 -0500 Subject: [PATCH] Refuse to post the same payment line twice, at the sender MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Members were seeing the same payout announced twice in the proof channel and the shared payments topic, and asking whether someone had been paid twice. The money was never wrong — one transfer, one contract event — but a payment feed is the last place where a second receipt is acceptable, because it makes people doubt the ledger. The indexer's durable announce-once key is the real fix and shipped in 85a6056. This adds the layer behind it: sendTelegramTo refuses to post the same text to the same chat within 15 minutes, and logs when it does. Distinct events can never collide — every line carries its own member ids, amount and transaction hash. Deliberately at the sender rather than the emitter, so it holds no matter which upstream path replays an event. Co-Authored-By: Claude Opus 5 (1M context) --- server.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/server.js b/server.js index d33d845..45be1fe 100644 --- a/server.js +++ b/server.js @@ -163,9 +163,34 @@ function sendTelegram(text, topicId, replyMarkup) { } // Same bot, any chat: the team forum, or the public payment-proof channel // (config.telegramProofChatId — the bot just has to be an admin there). +// Last-resort duplicate guard, at the point of send. +// +// 2026-09-18: members saw the same payout line posted twice in the proof channel and the +// shared payments topic — "did this person get paid twice?" The money was always right; only +// the announcement doubled. The indexer now has a durable announce-once key per on-chain +// event, which is the real fix, but a payment feed is the last place to rely on a single +// layer: anything upstream that replays an event would put a second receipt in front of +// members and make them doubt the ledger. So the sender itself refuses to post the same text +// to the same chat twice within the window. Distinct events never collide — every line +// carries its own ids, amount and transaction hash. +const TG_RECENT = new Map(); +const TG_DEDUPE_MS = 15 * 60 * 1000; +function tgSeenRecently(chatId, topicId, text) { + const k = String(chatId) + '|' + String(topicId || '') + '|' + text; + const now = Date.now(); + if (TG_RECENT.size > 500) for (const [kk, ts] of TG_RECENT) if (now - ts > TG_DEDUPE_MS) TG_RECENT.delete(kk); + const prev = TG_RECENT.get(k); + if (prev && now - prev < TG_DEDUPE_MS) return true; + TG_RECENT.set(k, now); + return false; +} function sendTelegramTo(chatId, text, topicId, replyMarkup, parseMode) { const c = getConfig(); if (!c.telegramBotToken || !chatId) return; + if (tgSeenRecently(chatId, topicId, text)) { + console.warn('telegram duplicate suppressed:', String(text).replace(/\s+/g, ' ').slice(0, 90)); + return; + } // No link previews: payout/team-build posts carry Polygonscan links, and the // preview card tripled the height of every message in the group. const payload = { chat_id: chatId, text, disable_web_page_preview: true };