DEV Community

Cover image for Telegram Mini App vs Bot for Crypto Swaps: Architecture Notes
Nefi Swap
Nefi Swap

Posted on

Telegram Mini App vs Bot for Crypto Swaps: Architecture Notes

If you're building a crypto swap flow inside Telegram, you have two interfaces to choose from: a bot (chat commands and inline buttons) or a Mini App (a web app rendered inside Telegram). Most production exchangers end up shipping both. Here's how they split the work, and the pitfalls that actually break swaps.

When to use which

Bot Mini App
Best for Status updates, quick repeat swaps, notifications First-time swaps, pair search, address input
UI Messages + inline keyboards Full HTML/JS UI
Searching 1,000s of assets Painful Natural (search + filter)
Push notifications Native Needs the bot anyway

A practical split: the Mini App handles the swap creation, and the bot handles everything after: deposit confirmation, status changes, completion.

1. Never trust initData on the client

The Mini App receives window.Telegram.WebApp.initData, a query string with user info and a hash. Your backend must validate it before creating any swap tied to a user.

The algorithm (from Telegram's docs):

  1. Remove hash from the params
  2. Sort the remaining key=value pairs alphabetically and join them with \n
  3. secret_key = HMAC_SHA256(key="WebAppData", msg=bot_token)
  4. Compare hex(HMAC_SHA256(key=secret_key, msg=data_check_string)) with hash
import crypto from "node:crypto";

export function validateInitData(initData, botToken, maxAgeSec = 3600) {
  const params = new URLSearchParams(initData);
  const hash = params.get("hash");
  params.delete("hash");

  const dataCheckString = [...params.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([k, v]) => `${k}=${v}`)
    .join("\n");

  const secretKey = crypto
    .createHmac("sha256", "WebAppData")
    .update(botToken)
    .digest();

  const computed = crypto
    .createHmac("sha256", secretKey)
    .update(dataCheckString)
    .digest("hex");

  if (computed.length !== hash?.length ||
      !crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(hash))) {
    return null;
  }

  const authDate = Number(params.get("auth_date"));
  if (Date.now() / 1000 - authDate > maxAgeSec) return null;

  return JSON.parse(params.get("user") ?? "null");
}
Enter fullscreen mode Exit fullscreen mode

Also check auth_date. A valid-but-old initData string is still replayable.

2. Quotes expire, so design for it

A swap quote is only valid for a short window. If a user opens the Mini App, gets distracted, and comes back ten minutes later, the rate on screen is stale.

  • Store quote_expires_at server-side, and don't trust the client's timer
  • Re-quote on "Confirm" if expired, and show the diff
  • Decide upfront how you handle a deposit that arrives after expiry (re-quote, refund, or floating rate)

3. Memo/tag networks cause most "missing deposit" tickets

Assets on networks like XRP, XLM, EOS, and TON (for exchange deposit addresses) often require a memo/destination tag in addition to the address. If the user omits it, funds land in the shared deposit address with no way to match them automatically.

  • Make the memo field required in the UI for those networks, not optional
  • Show it with the same visual weight as the address, plus a copy button
  • In the bot, send address and memo as separate messages so each can be copied on mobile

4. Network ambiguity is the second biggest issue

"USDT" isn't one asset. It's USDT-TRC20, USDT-ERC20, USDT-BEP20, and so on. EVM chains share the same 0x address format, so an address check alone won't catch a wrong-network send.

  • Make network selection explicit, never implicit
  • Display the network name next to every address you show

5. Deep links connect the two interfaces

Use startapp parameters to hand off from bot to Mini App with context:

https://t.me/YourBot/app?startapp=swap_BTC_USDTTRC20
Enter fullscreen mode Exit fullscreen mode

The parameter shows up as start_param in initData, so a bot message like "Swap again?" can open the Mini App pre-filled.

6. Push status through the bot, don't poll in the UI

Mini Apps close. Users leave. Swap status (awaiting deposit β†’ confirming β†’ exchanging β†’ sent) should be pushed as bot messages, triggered by your backend's state machine and not by a polling loop in a web view nobody is looking at.


For a live example of this split, NefiSwap runs swap creation in a Telegram Mini App and uses a bot for quick swaps and status updates across 8,000+ assets: nefiswap.com. Disclosure: written by the NefiSwap team.

Top comments (0)