Telegram
Telegram Identity Integration (GitBook Page)
You can associate your Telegram account with your DEMOS address by following these steps. This page provides two integration styles:
High-level (SDK + helper service approach)
Low-level (explicit challenge + bot attestation flow)
The Telegram flow differs from GitHub / Twitter because it involves an authorized Telegram Bot that produces an unsigned on‑chain identity transaction after verifying both the user’s Telegram login payload and a signed wallet challenge.
Telegram Bot Overview
Flow: dApp → Telegram Bot (verification with commands) -> Verification on dApp.
The Telegram bot (or a verification microservice) acts as an attestation authority. It must:
Accept a POST containing the Telegram Login Widget auth payload (
telegramUser) and thesignedChallengeproduced by the user wallet.Verify the Telegram auth payload hash (HMAC) server-side.
Verify the user wallet signature embedded in
signedChallenge(delegated to node during/api/tg-verify).Sign an attestation with the bot's Ed25519 key and call the node
/api/tg-verifyendpoint.Return the resulting unsigned identity transaction to the dApp.
Only the widget-based REST flow is covered here.
Minimal Bot Environment Variables
BOT_TOKEN
Telegram Bot API token
123456:ABCDEF...
GENESIS_PRIVATE_KEY
Bot Ed25519 private key (hex, 64 bytes)
0x...
GENESIS_PUBLIC_KEY
Bot public key whitelisted in genesis
0x...
NODE_URL
Demos node base URL
http://127.0.0.1:53550
BOT_USERNAME
Telegram Bot Username
yourTelegramBotUsername
Attestation Signing Logic (Core attest Function)
attest Function)The bot builds an ordered payload then signs it with its private key. Ordering matters to ensure deterministic signature.
async function attest(telegramId: string, username: string, signedChallenge: string) {
const payload = { telegram_id: telegramId, username, signed_challenge: signedChallenge, timestamp: Math.floor(Date.now()/1000) }
// Lexicographic ordering of keys for consistent signing
const ordered = Object.keys(payload).sort().reduce((a,k)=> (a[k] = (payload as any)[k], a), {} as any)
const msg = JSON.stringify(ordered)
const sig = nacl.sign.detached(Buffer.from(msg,'utf8'), Buffer.from(GENESIS_PRIVATE_KEY,'hex'))
const bot_signature = Buffer.from(sig).toString('hex')
const body = { ...payload, bot_address: GENESIS_PUBLIC_KEY, bot_signature }
const r = await fetch(`${NODE_URL}/api/tg-verify`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) })
const text = await r.text()
if(!r.ok) throw new Error('node '+r.status+' '+text)
const j = text ? JSON.parse(text) : {}
if(!j.success) throw new Error(j.error || 'verify_failed')
return j // { success:true, unsignedTransaction: {...} }
}Security guarantees enforced by node:
Bot key (public) must match a whitelisted genesis entry.
Challenge hash embedded in unsigned transaction prevents reuse.
User signature inside
signed_challengeties Telegram identity to wallet.
Telegram Auth Payload HMAC Verification (Server-Side)
Telegram instructs verifying the Login Widget payload like this:
import crypto from 'node:crypto'
function verifyTelegramAuth(auth: Record<string, any>, botToken: string) {
const { hash, ...rest } = auth
// 1. Sort keys alphabetically
const dataCheckArr = Object.keys(rest)
.sort()
.map(k => `${k}=${rest[k]}`)
const dataCheckString = dataCheckArr.join('\n')
// 2. Secret key = SHA256(botToken)
const secretKey = crypto.createHash('sha256').update(botToken).digest()
// 3. Compute HMAC-SHA256 of dataCheckString with secretKey
const hmac = crypto
.createHmac('sha256', secretKey)
.update(dataCheckString)
.digest('hex')
return hmac === hash
}If this returns false, reject the request and DO NOT proceed to attestation.
Verification REST Endpoint (Unified) Example
import express from 'express'
import nacl from 'tweetnacl'
import crypto from 'node:crypto'
const app = express(); app.use(express.json())
const BOT_TOKEN = process.env.BOT_TOKEN!
const GENESIS_PRIVATE_KEY = process.env.GENESIS_PRIVATE_KEY!.replace(/^0x/,'')
const GENESIS_PUBLIC_KEY = process.env.GENESIS_PUBLIC_KEY!
const NODE_URL = process.env.NODE_URL || 'http://127.0.0.1:53550'
function (auth: any) {
const { hash, ...rest } = auth
const dataCheckString = Object.keys(rest).sort().map(k => `${k}=${rest[k]}`).join('\n')
const secretKey = crypto.createHash('sha256').update(BOT_TOKEN).digest()
const hmac = crypto.createHmac('sha256', secretKey).update(dataCheckString).digest('hex')
return hmac === hash
}
async function callVerify(telegramUser: any, signedChallenge: string) {
const payload = { telegram_id: telegramUser.id, username: telegramUser.username || '', signed_challenge: signedChallenge, timestamp: Math.floor(Date.now()/1000) }
const ordered = Object.keys(payload).sort().reduce((a,k)=> (a[k]=(payload as any)[k], a), {} as any)
const msg = JSON.stringify(ordered)
const sig = nacl.sign.detached(Buffer.from(msg,'utf8'), Buffer.from(GENESIS_PRIVATE_KEY,'hex'))
const bot_signature = Buffer.from(sig).toString('hex')
const body = { ...payload, bot_address: GENESIS_PUBLIC_KEY, bot_signature }
const r = await fetch(`${NODE_URL}/api/tg-verify`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) })
const text = await r.text(); if(!r.ok) throw new Error(text || r.status.toString())
const j = text ? JSON.parse(text) : {}
if(!j.success) throw new Error(j.error || 'verify_failed')
return j
}
app.listen(8787, () => console.log('Verification service listening on :8787'))This single endpoint now handles HMAC validation, attestation signing, and forwarding to the node.
Optional Proxy / Forwarder Pattern (Server Relay)
If you keep the verification service private, expose a thin relay through your main backend (adapted from examples/BotWebhookForwarder.ts):
app.post('/api/telegram/verify', async (req, res) => {
const { telegramAuth, signedChallenge } = req.body
if(!telegramAuth || !signedChallenge) return res.status(400).json({ error:'Missing telegramAuth or signedChallenge' })
const resp = await fetch(`${BOT_INTERNAL_URL}/webhook/verification`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ telegramUser: telegramAuth, signedChallenge, timestamp: Math.floor(Date.now()/1000), source:'dapp-server' })
})
const data = await resp.json()
if(!resp.ok) return res.status(resp.status).json({ error: data.error || 'Bot verification failed' })
res.json(data)
})Benefits:
Hides internal verification service
Central HMAC verification & logging
Add auth / API keys / rate limiting easily
1. Connect your wallet
Use the DEMOS SDK (web) and obtain (or generate) a wallet/key pair. In production you’d use an existing wallet provider; below we show a mnemonic example only for demonstration.
import { Demos } from "@kynesyslabs/demosdk/websdk";
// the DEMOS rpc (HTTP endpoint of a node)
const rpc = "https://demosnode.discus.sh";
const demos = new Demos();
await demos.connect(rpc);
await demos.connectWallet(mnemonic); // Replace with secure wallet handling
console.log("Address:", demos.wallet.address);2. Obtain a one‑time challenge
Open the telegram bot on your telegram app, and get the challenge by writing /link command.
3. Sign the challenge (prove wallet ownership)
const signature = await demos.wallet.signMessage(challenge);
const signedChallenge = `${challenge}:${signature}`;
console.log('Signed challenge:', signedChallenge.slice(0, 96) + '…');Never reuse a challenge. If the user abandons the flow, discard it and request a new one later.
4. Authenticate the user with the Telegram Login Widget
Send the authentication payload to the telegram bot as message like this format <signature publicKey> where signature will be signed challenge which gave the bot, and the public key, will be your wallet public key.
5. Check telegram channel membership
Ensure the user is a member of a specific Telegram channel to proceed with the identity attestation.
export async function checkMembership(
botWebhook: string,
telegramUser: TelegramUserAuth,
): Promise<boolean> {
const r = await fetch(
`${botWebhook.replace(/\/$/, "")}/webhook/check-membership`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "YourAppNameAndVersion/1.0",
},
body: JSON.stringify({
telegramUser: telegramUser,
timestamp: Date.now(),
source: "your-app-name",
}),
},
);
if (!r.ok) throw new Error(`Challenge HTTP ${r.status}`);
const j = await r.json();
if (j.error) throw new Error(j.error);
return j.isMember;
}
const isMember = await checkMembership(botUrl, telegramUser);
if (!isMember) throw new Error('User is not a channel member');This function checks if the user is a member of the specified channel using Telegram's getChatMember API method. If the user is not a member, an error is thrown, preventing further steps in the attestation process.
6. Send an identity attestation request to your Telegram Bot
Your bot exposes an internal HTTPS endpoint (e.g. /webhook/verification). Send it:
async function submitAttestationRequest(botUrl: string, telegramUser: any, signedChallenge: string) {
const r = await fetch(`${botUrl}/webhook/verification`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ telegramUser, signedChallenge, timestamp: Math.floor(Date.now()/1000) })
});
if(!r.ok) throw new Error('Bot HTTP ' + r.status);
const j = await r.json();
if(!j.success) throw new Error(j.error || 'Bot attestation failed');
return j.unsignedTransaction; // Opaque unsigned identity tx
}
const unsignedTx = await submitAttestationRequest(botUrl, telegramUserPayload, signedChallenge);
console.log('Unsigned identity tx:', unsignedTx);Bot responsibilities:
Verify Telegram auth payload (hash + expiry)
Validate wallet signature over challenge
Ensure challenge not reused
Call node
/api/tg-verifyto obtain unsigned identity transactionReturn unsigned transaction to dApp
7. Sign & broadcast the identity transaction
// Sign with wallet, confirm with node, then broadcast
const signedTx = await demos.wallet.signTransaction(unsignedTx);
const validity = await demos.confirm(signedTx);
const broadcastRes = await demos.broadcast(validity);
console.log('Broadcast result:', broadcastRes);
const txHash = broadcastRes.hash || broadcastRes.tx_hash;
console.log('Telegram identity linked. Tx hash:', txHash);Successful broadcast response (example):
{
"result": 200,
"response": { "message": "Verified telegram proof. Transaction applied." },
"require_reply": false,
"extra": { "confirmationBlock": 42 }
}8. Getting linked web2 (Telegram) accounts
After the confirmation block has been forged you can query all linked web2 identities (includes telegram array if present):
import { Identities } from "@kynesyslabs/demosdk/abstraction";
const identities = new Identities();
const web2Ids = await identities.getWeb2Identities();
console.log(web2Ids);Example response snippet:
{
"result": 200,
"response": {
"telegram": [
{
"username": "example_user",
"telegramId": 123456789,
"proofHash": "ab3f...c901",
"challengeHash": "7ff2...19d0"
}
]
},
"require_reply": false,
"extra": null
}9. Removing linked Telegram identity
Create a removal transaction using the web2 abstraction (context = telegram):
const payload = {
context: 'telegram',
username: 'example_user' // Or other unique handle stored during linking
};
const validityData = await identities.removeWeb2Identity(demos, payload);
if (validityData.result === 200) {
const res = await demos.broadcast(validityData);
console.log(res);
}Successful removal response:
{
"result": 200,
"response": { "message": "Identity removed. Transaction applied." },
"require_reply": false,
"extra": { "confirmationBlock": 57 }
}End-to-End (Headless Helper)
If you prefer a single orchestrator call, adapt the following (mirrors examples/flowFunctions.ts):
import { runTelegramIdentityFlow } from './examples/flowFunctions';
const result = await runTelegramIdentityFlow({
nodeUrl: rpc,
botUrl,
wallet: demos.wallet,
demos,
telegramUser: telegramUserPayload,
onStage: (s, d) => console.log('[stage]', s, d)
});
console.log('Flow complete:', result.hash);Stages emitted:
challenge:request→challenge:received→challenge:signed→tx:unsigned→tx:broadcasted
Architecture Summary
getChallenge
Node
One-time challenge
Timestamp + nonce (anti replay)
signChallenge
Wallet
Prove address ownership
Ed25519 signature
Telegram auth
User + Widget
Prove Telegram control
HMAC hash w/ bot token
Attestation (bot)
Bot
Build unsigned tx
Bot key authorized in genesis
Sign & broadcast
Wallet + Node
Record identity on-chain
Node validates all proofs
Environment Variables (Frontend Example)
VITE_DEMOS_NODE_URL
Demos node base URL
https://demosnode.discus.sh
VITE_TELEGRAM_BOT_SERVER_URL
Your bot verification endpoint
https://your-bot.example.com
VITE_TELEGRAM_BOT_USERNAME
Bot username (for widget)
demos_prod_bot
VITE_FRONTEND_URL
Public origin hosting widget
https://app.example.com
Common Errors & Remedies
Challenge HTTP 4xx/5xx
Node unreachable or malformed address
Retry / show maintenance message
Bot HTTP 5xx
Bot offline / network issue
Exponential backoff retry
Bot attestation failed
Invalid Telegram auth or stale challenge
Force new Telegram login + new challenge
BroadcastFailure
Network congestion
Retry with fresh nonce (re-fetch challenge if needed)
User cancels sign
Wallet rejection
Offer retry; do not reuse challenge
Security Notes
Never trust the Telegram auth payload hash solely on the client; verify server-side where possible.
Expire unused challenges (e.g. 5–10 minutes) and reject reuse server-side.
Rate limit challenge requests per address/IP.
Log stage transitions with a correlation ID (address or session) for auditing.
Minimal UI Flow (Conceptual)
User clicks “Link Telegram”
UI must tell user to open the telegram bot
Place where user user can paste and sign the chalange from the bot
Show confirmation (tx hash + linked username)
Complete Flow Diagram
User | Web App | Telegram Modal | Demos Wallet | Telegram Bot | Backend
-----------------|------------------|------------------|-----------------|-----------------|-----------------
Click "Start Telegram Verification"
|-> Open modal
|----------------->|
| | Show instructions to get challenge
Show instructions|<-----------------|
Request challenge|-------------------------------------------------> Telegram Bot
| | | |-> generateTelegramChallenge(address)
| | | |-----------------> Backend
| | | |<----------------- {challenge, expiresAt}
Display challenge|<------------------------------------------------- Telegram Bot
Paste challenge |-> Telegram Modal
|----------------->|-> signMessage(challenge)
| |<----------------- signature
Provide signature|-> Web App
|----------------->|
Consume challenge|-> Backend
|----------------->|
Valid & unused | | | | |<-- true
Refresh address |-> Web App refresh info (hasTelegramIdentity)
Verification complete
|<-----------------| Telegram Modal
Success & auto-close
|<-----------------| Telegram Modal
Invalid/expired/used
| | | | |<-- false
Error & retry |<-----------------| Telegram Modal
Cleanup / Removal
To unlink, use removeWeb2Identity with context: 'telegram' (see removal step). The chain state will reflect the removal in a new block.
Next Steps
Integrate server-side Telegram HMAC validation
Replace mnemonic demo wallet with production wallet provider
Add UI states for: waiting bot, signing, broadcast pending, final confirmation
Implement telemetry on each stage for monitoring
This completes the Telegram identity linking guide.
Last updated