An escrow bot that cannot see faces
Anonymity in a mediated trade is not a property of the transport. It is a property of one table, the map from a relayed message to its sender, and every feature is judged by it.
ShreezyPay Bot mediates trades between two Telegram groups that must not learn who is in the other. Each side talks to the bot, the bot relays what needs relaying, and if the trade goes wrong the bot holds enough to settle a dispute. The property that makes it work is not encryption, which Telegram supplies to everyone, and it is not the relay itself. It is the size of one table: the mapping from a relayed message back to the person who sent it. Anonymity between two parties in a mediated trade is a property of that table, its contents, its lifetime and its distance from everything else the bot stores, and every other feature in the bot exists to stop the two parties from leaking the mapping themselves.
The unmask table
I call it the unmask table because that is what its disclosure would do: unmask a pair. The bot needs some version of it, because a reply from one side has to reach the right thread on the other, and a dispute has to be attributable to a party. The design question is how little the table can hold while still doing those two jobs, and the answer is much less than the obvious implementation stores.
The obvious implementation maps Telegram user identifiers and usernames to trades. The minimal one maps a thread identifier in one group to an opaque pair identifier, and the pair identifier to a thread identifier in the other group. No usernames, no user identifiers, no display names. When a message update arrives from the Bot API for a thread, the bot looks up the pair, finds the mirror thread, rewrites the message with the sender stripped and the thread identifier swapped, and sends it. The bot never needs to know who a person is, only which thread they are in, and a thread is not a person.
The bot still has to enforce the send permissions, which means checking who sent a message, and that check reads the sender's identifier from the update, compares it against the thread's allow list, and drops it; the identifier is never written to disk and never crosses. Three properties follow, and each is a design constraint rather than a hope. The table is small, holding two thread identifiers and one pair identifier per active trade, and nothing about people. The table is short-lived, created when a pair is opened and deleted when the trade settles, so that a dispute a month later cannot be traced to a thread that no longer maps to anything. And the table is separate from the escrow state, which knows the pair identifier, the amount and the status, and never a thread identifier, so that the store a dispute actually needs cannot by itself connect a party to a group.
Everything else is leak prevention
Once the bot's own table is minimal, the remaining way for a pair to be unmasked is the parties doing it themselves, and the rest of the bot's features exist for that. Per-user send permissions decide who in a group may post into a relayed thread at all, because a member who was never meant to speak to the other side is the likeliest source of an unguarded message. Keyword filtering scans outgoing relayed text for the things that leak identity, handles, phone numbers, invite links, the name of the group, and blocks or masks them before they cross. Reply-thread sync keeps replies attached to the right relayed message on both sides, which sounds like a convenience and is a privacy feature, because a reply that lands in the wrong thread quotes text from a different trade to a party who should never have seen it.
The matrix is the review tool. When a feature is proposed, a read receipt, a typing indicator, a history export, the question is which row it touches and whether it moves the field's treatment toward "must know". A history export that includes sender identifiers moves two rows at once and is refused. A typing indicator that shows which side is typing but not who is fine, because "a side" is a pair identifier, not a person. The rule is that every feature is judged by whether it grows or shrinks the unmask table, and features that grow it need a reason as strong as the dispute process, which is the only thing that has ever justified the table's existence.
The limits Telegram imposes
A relay lives inside the platform's rate ceilings, and Telegram's Bot API FAQ publishes them: about one message per second to a single chat, with brief bursts tolerated before the API answers with errors; no more than twenty messages per minute to the same group; and about thirty messages per second for bulk notifications, unless the bot pays for a higher broadcast tier. For a relay those numbers are architecture. The per-group ceiling of twenty a minute is the tightest, and it is per group, not per thread, so a busy pair of groups with several concurrent trades shares one budget of a message every three seconds in each direction.
That budget interacts with anonymity in a way that is easy to miss. A relay that queues messages under the ceiling has to decide what to do when the queue is long, and the tempting answer, batching several relayed messages into one, is a leak: a batch reveals that three messages came from one side in quick succession, which is timing information about the other party's activity, and if the batch preserves order across threads it reveals which trades are active together. The design keeps one relayed message per outgoing message, delays rather than merges, and accepts the ceiling as the price of not letting the queue itself become a side channel.
What a dispute looks like
The test of the design is the dispute, because that is when the bot is asked to know the most. A party claims the other side did not deliver. The bot has the escrow state for the pair, the amount and the status, and it has the relayed thread on each side, which is the record of what was said. It does not have, and cannot produce, the identity of the party on the other side, because the unmask table maps threads to pairs and nothing to people. The mediator, a human or a rule, reads the two threads, decides, and instructs the escrow store to settle one way or the other by pair identifier. The parties are never introduced. The bot could not introduce them if it wanted to, and the design's whole purpose is that "could not" rather than "will not" is the guarantee.
There is one honest limit. Telegram itself knows who is in every group and who sent every message, and a party who is compelled or persuaded to hand over their own group's history has their own side's identities in hand. The unmask table protects the pairing, the link between one side and the other, and it protects it against the bot's operator, against the bot's database, and against the other party. It cannot protect either side from itself, and the send permissions and keyword filter are the acknowledgement that the biggest risk to a party's anonymity was always going to be a member of their own group typing their own name.
Get new posts by email
Occasional essays on engineering, AI, and building for the people technology leaves behind.
Subscribe with RSS