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:
- Telegram panel — shows Telegram user info when opened inside Telegram
- Wallet Auth panel — connect wallet + sign in
- 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
- Open @BotFather in Telegram
- Send
/newbot - Choose a bot name
- Choose a bot username (must end with
bot) - Save the bot token (you’ll put this in
.envasTELEGRAM_BOT_TOKEN)
Then set up the Mini App:
- Select the bot you just created
- Enter a name for your web app
- Add a short description
- Upload a logo (optional — you can skip)
- Provide a valid HTTPS URL for your frontend (Vercel / Netlify)
- 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 URL —
localhostwill 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
-
greetingstores the current on-chain message -
owneris 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.useris 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
initDataon 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
-
injectedworks in normal desktop browsers with MetaMask/Fluent -
walletConnectworks inside Telegram on mobile -
projectIdcomes 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:
- Ask backend for a challenge message (nonce)
- Sign that message with the wallet
- 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
initDatais attached so backend can link wallet
Telegram user - If you see
API unreachable (http://localhost:3001), your Mini App is still pointing to localhost — set a publicVITE_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
-
verifyMessagerecovers/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
- Deploy frontend + backend
- Put public frontend URL in BotFather Mini App settings
- Open bot → launch Mini App
- Connect wallet with WalletConnect
- Sign in
- 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:
- Build a simple Conflux smart contract
- Create a Telegram bot + Mini App with BotFather
- Use Telegram SDK in the frontend
- Use WalletConnect for mobile wallet access
- Prove wallet ownership with challenge + signature
- Verify Telegram init data securely on the backend
- 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.
Telegram user