1 September 2026 · 5 min read

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.

Two groups, the bot and the unmask table: a reply crossing with identity stripped and the thread id rewritten Left, group A with a thread; right, group B with a thread; the bot in the middle holds a small table mapping thread A to pair P and pair P to thread B. A message from a member of group A enters its thread with a sender and text; the bot looks up the pair, strips the sender, rewrites the thread reference to B's thread, and posts only the text in group B. The escrow state is drawn as a separate store the table does not touch. What crosses, and what stays behind Group A thread 4127 sender: a member text: "sent, check" The bot UNMASK TABLE A:4127 to pair P9 pair P9 to B:588 no names, no user ids Group B thread 588 sender: the bot text: "sent, check" Escrow state pair P9: amount, status The escrow store knows the pair and the money, never a thread, so it unmasks nobody.
Illustrative: the relay path and the two stores as designed; the identifiers are invented.

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.

What the bot must know, may cache and must never store, per message field A matrix with message fields as rows and three columns. Thread id: must know, kept only while the trade is open. Pair id: must know, kept with the escrow state. Message text: may cache briefly for reply sync, never stored beyond the relay. Sender user id: must never store; it is read to check send permission and discarded. Sender username and display name: must never store or relay. Group name and invite links: must never relay, filtered out. Amount and status: must know, in the escrow store only. Every field sorted by what would unmask a pair FIELD TREATMENT WHY Thread idmust know, until settledroutes the reply Pair idmust knowthe only key escrow sees Message textmay cache, brieflyreply sync, then gone Sender user idread, never storedchecks permission, discarded Username, display namenever stored or relayedwould unmask on sight Group name, invite linksnever relayed, filteredthe keyword filter's job The test for a new feature: does it move any field toward "must know"?
Illustrative: the field-level policy as the design applies it; the rows are the fields a Telegram message update carries that matter to anonymity.

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.

Telegram Bot API rate ceilings that shape a relay, in messages per second on a log scale Horizontal bars: to one chat, about 1 per second; to the same group, 20 per minute, about 0.33 per second; bulk notifications, about 30 per second; paid broadcast tier, up to 1,000 per second. The per-group ceiling is highlighted as the one that binds a relay. The per-group ceiling is the one a relay hits Messages per second, log scale from 0.1 to 1,000, from the Bot API FAQ Same group 0.33, twenty a minute One chat about 1 Bulk notifications about 30 Paid broadcast tier up to 1,000 A pair of groups shares one twenty-a-minute budget each way, whatever the trade count. eighty pixels per decade
Source: the Telegram Bot API FAQ, section on hitting limits, September 2026.

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.

TelegramPrivacySystem Design
All writing

Written by Mohd Shayan

Get new posts by email

Occasional essays on engineering, AI, and building for the people technology leaves behind.

One email per new post. Unsubscribe any time.

Subscribe with RSS