How to Build a Conflux Telegram Mini App (Wallet Auth + Smart Contract)

Hello everyone, welcome to this guide.

Today we are going to see how to build a Telegram Mini App with wallet authentication and a smart contract on Conflux eSpace. We’ll first look at the live UI and Telegram setup, then walk through the important coding parts so you can build the same kind of dApp yourself.

Live demo example: https://cfx-telegra-m-starter-mini-app.vercel.app/


What You’ll Build

This starter includes:

Feature What it does
Telegram SDK Opens the app inside Telegram and reads user/theme data
Wallet auth Connects wallet + proves ownership with a signature
Smart contract Simple on-chain Greeting contract on Conflux eSpace
Backend API Validates Telegram init data and wallet signatures
Mini App UI Mobile-first React UI that works inside Telegram

Project structure

CFXTelegraMStarter/
├── apps/
│   ├── mini-app/     # Telegram Mini App (React + Vite)
│   └── api/          # Backend API (Express)
├── packages/
│   ├── contracts/    # Hardhat + Greeting.sol
│   └── shared/       # Shared types, ABI, chain config
└── .env.example

Part 1 — UI Overview

The Mini App UI has a few simple sections:

  1. Telegram panel — shows Telegram user info when opened inside Telegram
  2. Wallet Auth panel — connect wallet + sign in
  3. Smart Contract panel — read/update the on-chain greeting

We won’t dig into every UI detail here. The goal is to understand how the app works end-to-end inside Telegram.


Part 2 — Create the Telegram Bot + Mini App

To create a Mini App, you need BotFather. BotFather plays a very important role in building Telegram bots and Mini Apps.

Step-by-step

  1. Open @BotFather in Telegram
  2. Send /newbot
  3. Choose a bot name
  4. Choose a bot username (must end with bot)
  5. Save the bot token (you’ll put this in .env as TELEGRAM_BOT_TOKEN)

Then set up the Mini App:

  1. Select the bot you just created
  2. Enter a name for your web app
  3. Add a short description
  4. Upload a logo (optional — you can skip)
  5. Provide a valid HTTPS URL for your frontend (Vercel / Netlify)
  6. Choose a short name for the Mini App

After that, when someone opens your bot and launches the app, your web app opens inside Telegram itself. If you expand it, it looks almost like a full mobile app.

Important production note

Inside Telegram:

  • MetaMask browser extension usually does not work like in Chrome
  • Use WalletConnect for mobile wallet connection
  • Use Conflux eSpace Testnet (chain ID 71) for testing
  • Your backend must be on a public URLlocalhost will not work from Telegram/Vercel

Part 3 — Smart Contract (Greeting)

The smart contract is intentionally simple. It stores a greeting string on-chain. Anyone can update it, and the Mini App can read/show that value.

File: packages/contracts/contracts/Greeting.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Greeting {
    string public greeting;
    address public owner;

    event GreetingUpdated(string newGreeting, address indexed updatedBy);

    constructor(string memory _greeting) {
        greeting = _greeting;
        owner = msg.sender;
    }

    function setGreeting(string memory _greeting) external {
        greeting = _greeting;
        emit GreetingUpdated(_greeting, msg.sender);
    }
}

Explanation

  • greeting stores the current on-chain message
  • owner is set to the deployer address
  • setGreeting() updates the message and emits an event
  • This is only a test contract — the real product logic can replace it later

Deploy (testnet)

npm run contracts:deploy

Then put the deployed address in your env:

GREETING_CONTRACT_ADDRESS=0xYourContractAddress
VITE_GREETING_CONTRACT_ADDRESS=0xYourContractAddress

Part 4 — Telegram SDK (Frontend)

The Telegram WebApp SDK initializes the Mini App, expands it, and gives us user/theme data.

File: apps/mini-app/src/hooks/useTelegram.ts

import { useEffect, useMemo } from 'react';
import WebApp from '@twa-dev/sdk';

export function useTelegram() {
  useEffect(() => {
    WebApp.ready();
    WebApp.expand();
    WebApp.enableClosingConfirmation();
  }, []);

  return useMemo(
    () => ({
      webApp: WebApp,
      user: WebApp.initDataUnsafe.user,
      initData: WebApp.initData,
      colorScheme: WebApp.colorScheme,
      themeParams: WebApp.themeParams,
      platform: WebApp.platform,
      haptic: WebApp.HapticFeedback,
      close: () => WebApp.close(),
    }),
    [],
  );
}

Explanation

  • WebApp.ready() tells Telegram the Mini App is ready
  • WebApp.expand() opens the app in full height
  • initDataUnsafe.user is useful for UI display only
  • initData (raw string) must be sent to the backend for real verification

Never trust frontend-only Telegram data for secure actions. Always validate initData on the server with your bot token.


Part 5 — WalletConnect Config (Why Telegram Needs It)

In Telegram’s in-app browser, injected wallets (like MetaMask extension) are often unavailable. That is why WalletConnect is important.

File: apps/mini-app/src/config.ts

import { confluxESpaceTestnet } from '@cfx/shared';
import { createConfig, http } from 'wagmi';
import { injected, walletConnect } from 'wagmi/connectors';

const walletConnectProjectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? '';

export const targetChain = {
  ...confluxESpaceTestnet,
  id: Number(import.meta.env.VITE_ESPACE_CHAIN_ID ?? confluxESpaceTestnet.id),
  rpcUrls: {
    default: {
      http: [import.meta.env.VITE_ESPACE_RPC_URL ?? confluxESpaceTestnet.rpcUrls.default.http[0]],
    },
  },
};

const connectors = [
  injected({ shimDisconnect: true }),
  ...(walletConnectProjectId
    ? [
        walletConnect({
          projectId: walletConnectProjectId,
          metadata: {
            name: 'CFX Telegram Starter',
            description: 'Conflux Mini App wallet auth',
            url:
              typeof window !== 'undefined'
                ? window.location.origin
                : 'https://cfx-telegram-starter.local',
            icons: ['https://avatars.githubusercontent.com/u/37784886'],
          },
          showQrModal: true,
        }),
      ]
    : []),
];

export const wagmiConfig = createConfig({
  chains: [targetChain],
  connectors,
  transports: {
    [targetChain.id]: http(targetChain.rpcUrls.default.http[0]),
  },
});

Explanation

  • injected works in normal desktop browsers with MetaMask/Fluent
  • walletConnect works inside Telegram on mobile
  • projectId comes from Reown Cloud (WalletConnect)
  • Default chain is Conflux eSpace Testnet (71)

Set this in .env / Vercel:

VITE_WALLETCONNECT_PROJECT_ID=your_project_id

Part 6 — Sign-In Flow (Prove Wallet Ownership)

Connecting a wallet only gives you an address.
Signing in proves the user actually owns that wallet.

This happens in 3 steps:

  1. Ask backend for a challenge message (nonce)
  2. Sign that message with the wallet
  3. Send signature + Telegram init data to backend for verification

File: apps/mini-app/src/hooks/useWallet.ts

const authenticateWallet = useCallback(async () => {
  if (!address) return;

  setIsAuthenticating(true);
  setAuthError(null);

  try {
    // 1) Get challenge message from backend
    const challengeRes = await fetch(`${API_URL}/api/wallet/challenge`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ address }),
    });

    if (!challengeRes.ok) {
      throw new Error('Failed to request auth challenge. Is the API running and reachable?');
    }

    const challenge = (await challengeRes.json()) as { message: string };

    // 2) Sign the challenge with the connected wallet
    const signature = await signMessageAsync({ message: challenge.message });

    // 3) Verify signature + optionally link Telegram user
    const verifyRes = await fetch(`${API_URL}/api/wallet/verify`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...(initData ? { Authorization: `tma ${initData}` } : {}),
      },
      body: JSON.stringify({
        address,
        signature,
        message: challenge.message,
      }),
    });

    if (!verifyRes.ok) {
      throw new Error('Wallet verification failed');
    }

    const result = (await verifyRes.json()) as { token: string };
    setSessionToken(result.token);
  } catch (error) {
    setAuthError(getApiErrorMessage(error));
  } finally {
    setIsAuthenticating(false);
  }
}, [address, initData, signMessageAsync]);

Explanation

  • This proves wallet ownership without sending a blockchain transaction
  • No gas fee is needed for login
  • Telegram initData is attached so backend can link wallet :left_right_arrow: Telegram user
  • If you see API unreachable (http://localhost:3001), your Mini App is still pointing to localhost — set a public VITE_API_URL

Part 7 — Backend: Telegram Auth Middleware

Frontend Telegram data is not enough. The backend must validate raw init data using your bot token.

File: apps/api/src/middleware/auth.ts

import { validate, parse } from '@tma.js/init-data-node';

export function telegramAuthMiddleware(req, res, next) {
  const [authType, authData = ''] = (req.header('authorization') ?? '').split(' ');

  if (authType !== 'tma' || !authData) {
    res.status(401).json({ error: 'Missing Telegram init data' });
    return;
  }

  try {
    validate(authData, process.env.TELEGRAM_BOT_TOKEN!, { expiresIn: 3600 });
    const initData = parse(authData);
    req.telegramUser = initData.user;
    next();
  } catch {
    res.status(401).json({ error: 'Invalid Telegram init data' });
  }
}

Explanation

  • Client sends: Authorization: tma <initData>
  • Server validates the signature using TELEGRAM_BOT_TOKEN
  • If valid, we can safely trust Telegram user identity
  • This is one of the most important security parts of any Telegram Mini App

Part 8 — Backend: Wallet Challenge + Signature Verify

File: apps/api/src/services/wallet.ts

import { randomBytes } from 'crypto';
import { verifyMessage } from 'viem';
import { buildWalletAuthMessage } from '@cfx/shared';

export function createWalletChallenge(address: string) {
  const nonce = randomBytes(16).toString('hex');
  const message = buildWalletAuthMessage({
    address,
    nonce,
    chainId: 71,
  });

  return {
    nonce,
    message,
    expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
  };
}

export async function verifyWalletSignature(params: {
  address: string;
  message: string;
  signature: `0x${string}`;
}) {
  return verifyMessage({
    address: params.address as `0x${string}`,
    message: params.message,
    signature: params.signature,
  });
}

Explanation

  • Challenge includes a random nonce so old signatures cannot be reused easily
  • Message expires after a short time
  • verifyMessage recovers/checks that the signature belongs to that address
  • After success, backend can issue a JWT session token

Useful API endpoints

Method Path Purpose
GET /api/health Health check
GET /api/me Validate Telegram init data
POST /api/wallet/challenge Create sign-in message
POST /api/wallet/verify Verify wallet signature
GET /api/contract/greeting Read greeting (optional API path)

Final Telegram step

  1. Deploy frontend + backend
  2. Put public frontend URL in BotFather Mini App settings
  3. Open bot → launch Mini App
  4. Connect wallet with WalletConnect
  5. Sign in
  6. Update greeting on-chain

Resources

Demo Video; https://youtu.be/ot7eP_o1ZyQ
GitHub repo; https://github.com/Vikash-8090-Yadav/CFXTelegraMStarter

Final Thoughts

That is how this project works:

  1. Build a simple Conflux smart contract
  2. Create a Telegram bot + Mini App with BotFather
  3. Use Telegram SDK in the frontend
  4. Use WalletConnect for mobile wallet access
  5. Prove wallet ownership with challenge + signature
  6. Verify Telegram init data securely on the backend
  7. Deploy everything to public HTTPS URLs

With this same pattern, you can build bigger Telegram dApps — games, marketplaces, loyalty apps, or on-chain tools — on Conflux.

Bye everyone, thanks for reading. If this guide helped you, share it with someone building Telegram Mini Apps. See you in the next one.