Backend Implementation
Build the Telegram bot with AI integration
Building the Backend
The backend consists of four main parts: encryption service, storage services, RISEx API integration, and the Telegram bot with LLM routing. The complete implementation can be found in the GitHub repository.
Encryption Service
Implement AES-256-GCM encryption for secure key storage:
import { createCipheriv, createDecipheriv, randomBytes, createHash } from 'crypto';
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16;
const AUTH_TAG_LENGTH = 16;
const SALT_LENGTH = 32;
function deriveKey(secret: string, salt: Buffer): Buffer {
return createHash('sha256')
.update(secret + salt.toString('hex'))
.digest();
}
export function encrypt(text: string, secret: string): string {
const salt = randomBytes(SALT_LENGTH);
const key = deriveKey(secret, salt);
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Combine salt + iv + authTag + encrypted
return Buffer.concat([
salt,
iv,
authTag,
Buffer.from(encrypted, 'hex')
]).toString('hex');
}
export function decrypt(encryptedHex: string, secret: string): string {
const buffer = Buffer.from(encryptedHex, 'hex');
const salt = buffer.subarray(0, SALT_LENGTH);
const iv = buffer.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
const authTag = buffer.subarray(
SALT_LENGTH + IV_LENGTH,
SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH
);
const encrypted = buffer.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);
const key = deriveKey(secret, salt);
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString('utf8');
}This implements authenticated encryption to prevent tampering with stored keys.
Storage Services
Signer Storage:
import fs from 'fs';
import path from 'path';
import { encrypt, decrypt } from './encryption';
const SIGNERS_FILE = path.join(__dirname, '../data/signers.json');
const SECRET = process.env.ENCRYPTION_SECRET!;
interface SignersData {
[walletAddress: string]: string; // encrypted private key
}
function readSigners(): SignersData {
if (!fs.existsSync(SIGNERS_FILE)) {
return {};
}
return JSON.parse(fs.readFileSync(SIGNERS_FILE, 'utf8'));
}
function writeSigners(data: SignersData): void {
fs.writeFileSync(SIGNERS_FILE, JSON.stringify(data, null, 2));
}
export function storeSigner(walletAddress: string, privateKey: string): void {
const signers = readSigners();
signers[walletAddress.toLowerCase()] = encrypt(privateKey, SECRET);
writeSigners(signers);
}
export function getSigner(walletAddress: string): string | null {
const signers = readSigners();
const encrypted = signers[walletAddress.toLowerCase()];
if (!encrypted) return null;
return decrypt(encrypted, SECRET);
}
export function hasSigner(walletAddress: string): boolean {
const signers = readSigners();
return !!signers[walletAddress.toLowerCase()];
}Telegram Link Storage:
import fs from 'fs';
import path from 'path';
const LINKS_FILE = path.join(__dirname, '../data/verified-links.json');
interface VerifiedLinks {
[telegramId: string]: string; // wallet address
}
function readLinks(): VerifiedLinks {
if (!fs.existsSync(LINKS_FILE)) {
return {};
}
return JSON.parse(fs.readFileSync(LINKS_FILE, 'utf8'));
}
function writeLinks(data: VerifiedLinks): void {
fs.writeFileSync(LINKS_FILE, JSON.stringify(data, null, 2));
}
export function linkTelegramToWallet(telegramId: string, walletAddress: string): void {
const links = readLinks();
links[telegramId] = walletAddress.toLowerCase();
writeLinks(links);
}
export function getWalletForTelegram(telegramId: string): string | null {
const links = readLinks();
return links[telegramId] || null;
}
export function hasLink(telegramId: string): boolean {
const links = readLinks();
return !!links[telegramId];
}RISEx Service
Create the RISEx API integration with EIP-712 signing:
import axios from 'axios';
import { privateKeyToAccount } from 'viem/accounts';
import { formatUnits, parseUnits } from 'viem';
const RISEX_API = process.env.RISEX_API_URL!;
// EIP-712 domain for RISEx
const domain = {
name: 'RISEx',
version: '1',
chainId: 8008135,
verifyingContract: '0x3d47aa78fe75e8616dbadfbdce4ede1e73b35b27' as `0x${string}`,
};
// EIP-712 types for order placement
const types = {
VerifySignature: [
{ name: 'signer', type: 'address' },
{ name: 'data', type: 'bytes' },
],
};
export async function placeOrder(
signerPrivateKey: string,
walletAddress: string,
market: 'BTC' | 'ETH',
side: 'long' | 'short',
size: string,
price: string,
orderType: 'market' | 'limit'
) {
const account = privateKeyToAccount(signerPrivateKey as `0x${string}`);
// Prepare order payload
const orderPayload = {
account: walletAddress,
market_id: market === 'BTC' ? 'BTC-PERP' : 'ETH-PERP',
side: side === 'long' ? 0 : 1,
size: parseUnits(size, 18).toString(),
price: parseUnits(price, 18).toString(),
order_type: orderType === 'market' ? 0 : 1,
expiry: Math.floor(Date.now() / 1000) + 300, // 5 minutes
};
// Encode data for signing
const data = JSON.stringify(orderPayload);
// Sign EIP-712 message
const signature = await account.signTypedData({
domain,
types,
primaryType: 'VerifySignature',
message: {
signer: account.address,
data: `0x${Buffer.from(data).toString('hex')}`,
},
});
// Call RISEx API
const response = await axios.post(`${RISEX_API}/v1/orders/place`, {
...orderPayload,
signature,
signer: account.address,
});
return response.data;
}
export async function getBalance(walletAddress: string) {
const response = await axios.get(`${RISEX_API}/v1/account/balance`, {
params: {
account: walletAddress,
token: '0x6bf6e258b3c5650b448cb1112835048ba5619dc1', // USDC
},
});
const balance = response.data?.data?.balance || '0';
return formatUnits(BigInt(balance), 18);
}
export async function getPositions(walletAddress: string) {
const response = await axios.get(`${RISEX_API}/v1/positions`, {
params: { account: walletAddress },
});
return response.data?.data?.positions || [];
}
export async function depositUSDC(
signerPrivateKey: string,
walletAddress: string,
amount: string
) {
const account = privateKeyToAccount(signerPrivateKey as `0x${string}`);
const depositPayload = {
account: walletAddress,
amount: parseUnits(amount, 18).toString(),
};
const data = JSON.stringify(depositPayload);
const signature = await account.signTypedData({
domain: {
...domain,
verifyingContract: '0x8e6c7d87fc4c35ab519127629c56bca07006cfca' as `0x${string}`,
},
types,
primaryType: 'VerifySignature',
message: {
signer: account.address,
data: `0x${Buffer.from(data).toString('hex')}`,
},
});
const response = await axios.post(`${RISEX_API}/v1/account/deposit`, {
...depositPayload,
signature,
signer: account.address,
});
return response.data;
}For additional functions like getOpenOrders(), see the complete implementation on GitHub.
LLM Router
Implement natural language processing with GPT-4o-mini:
import OpenAI from 'openai';
import * as risex from '../services/risexService';
import { getWalletForTelegram } from '../services/verifiedLinksStore';
import { getSigner } from '../services/signersStore';
const openai = new OpenAI({
baseURL: 'https://openrouter.ai/api/v1',
apiKey: process.env.OPENROUTER_API_KEY,
});
// Define tool schemas
const tools = [
{
type: 'function',
function: {
name: 'get_balance',
description: 'Get the USDC balance for the user account',
parameters: { type: 'object', properties: {} },
},
},
{
type: 'function',
function: {
name: 'place_order',
description: 'Place a perpetual futures order (market or limit)',
parameters: {
type: 'object',
properties: {
market: { type: 'string', enum: ['BTC', 'ETH'] },
side: { type: 'string', enum: ['long', 'short'] },
size: { type: 'string', description: 'Contract size (e.g., "0.02")' },
price: { type: 'string', description: 'Limit price or "market"' },
orderType: { type: 'string', enum: ['market', 'limit'] },
},
required: ['market', 'side', 'size', 'orderType'],
},
},
},
{
type: 'function',
function: {
name: 'get_positions',
description: 'Get all open perpetual positions',
parameters: { type: 'object', properties: {} },
},
},
{
type: 'function',
function: {
name: 'deposit',
description: 'Deposit testnet USDC to trading account',
parameters: {
type: 'object',
properties: {
amount: { type: 'string', description: 'Amount of USDC (default: 1000)' },
},
},
},
},
];
export async function handleMessage(telegramId: string, message: string): Promise<string> {
// Get wallet and signer
const walletAddress = getWalletForTelegram(telegramId);
if (!walletAddress) {
return '❌ No wallet linked. Use /link to get started.';
}
const signerKey = getSigner(walletAddress);
if (!signerKey) {
return '❌ No trading signer registered. Please complete registration.';
}
// Call GPT-4o-mini
const response = await openai.chat.completions.create({
model: 'openai/gpt-4o-mini',
messages: [
{
role: 'system',
content: 'You are a trading assistant for RISEx perpetual futures. Parse user requests and call the appropriate tools.',
},
{ role: 'user', content: message },
],
tools: tools as any,
tool_choice: 'auto',
});
const toolCall = response.choices[0]?.message?.tool_calls?.[0];
if (!toolCall) {
return response.choices[0]?.message?.content || '❓ Could not understand request.';
}
const functionName = toolCall.function.name;
const args = JSON.parse(toolCall.function.arguments);
// Execute tool
try {
switch (functionName) {
case 'get_balance':
const balance = await risex.getBalance(walletAddress);
return `💰 Balance: ${balance} USDC`;
case 'place_order':
const orderResult = await risex.placeOrder(
signerKey,
walletAddress,
args.market,
args.side,
args.size,
args.price || '0',
args.orderType
);
return `✅ Order placed!\nMarket: ${args.market}-PERP\nSide: ${args.side.toUpperCase()}\nSize: ${args.size}\nTx: ${orderResult.data?.tx_hash || 'N/A'}`;
case 'get_positions':
const positions = await risex.getPositions(walletAddress);
if (positions.length === 0) {
return '📊 No open positions.';
}
return positions.map((p: any) =>
`${p.market_id} ${p.side === 0 ? 'LONG' : 'SHORT'} ${formatUnits(BigInt(p.size), 18)}`
).join('\n');
case 'deposit':
const depositAmount = args.amount || '1000';
await risex.depositUSDC(signerKey, walletAddress, depositAmount);
return `✅ Deposited ${depositAmount} USDC to your account!`;
default:
return '❓ Unknown command.';
}
} catch (error: any) {
console.error('Tool execution error:', error);
return `❌ Error: ${error.message}`;
}
}Main Bot Implementation
Create the Telegram bot entry point:
import { Telegraf } from 'telegraf';
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import { handleMessage } from './llm/router';
import * as risex from './services/risexService';
import { getWalletForTelegram, linkTelegramToWallet, hasLink } from './services/verifiedLinksStore';
import { getSigner, storeSigner, hasSigner } from './services/signersStore';
dotenv.config();
const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN!);
const app = express();
app.use(cors({ origin: process.env.FRONTEND_URL }));
app.use(express.json());
// Slash commands
bot.command('start', (ctx) => {
ctx.reply(
`👋 Welcome to RISEx Trading Bot!\n\n` +
`Use /link to connect your wallet, then:\n` +
`• Natural language: "buy 0.02 BTC"\n` +
`• /balance - Check USDC balance\n` +
`• /deposit - Get testnet USDC\n` +
`• /positions - View open positions`
);
});
bot.command('link', (ctx) => {
const telegramId = ctx.from.id.toString();
ctx.reply(
`🔗 Your Telegram ID: \`${telegramId}\`\n\n` +
`Visit: ${process.env.FRONTEND_URL}\n` +
`Enter this ID and complete registration.`,
{ parse_mode: 'Markdown' }
);
});
bot.command('debug', (ctx) => {
const telegramId = ctx.from.id.toString();
const wallet = getWalletForTelegram(telegramId);
ctx.reply(
`🐛 Debug Info:\n` +
`Telegram ID: ${telegramId}\n` +
`Wallet: ${wallet || 'Not linked'}\n` +
`Signer: ${wallet && hasSigner(wallet) ? 'Registered' : 'Not registered'}`
);
});
bot.command('balance', async (ctx) => {
const telegramId = ctx.from.id.toString();
const wallet = getWalletForTelegram(telegramId);
if (!wallet) {
return ctx.reply('❌ No wallet linked. Use /link first.');
}
const balance = await risex.getBalance(wallet);
ctx.reply(`💰 Balance: ${balance} USDC`);
});
bot.command('deposit', async (ctx) => {
const telegramId = ctx.from.id.toString();
const wallet = getWalletForTelegram(telegramId);
if (!wallet) {
return ctx.reply('❌ No wallet linked. Use /link first.');
}
const signerKey = getSigner(wallet);
if (!signerKey) {
return ctx.reply('❌ No signer registered. Complete registration first.');
}
const amount = ctx.message.text.split(' ')[1] || '1000';
await risex.depositUSDC(signerKey, wallet, amount);
ctx.reply(`✅ Deposited ${amount} USDC!`);
});
// Natural language messages
bot.on('text', async (ctx) => {
const text = ctx.message.text;
// Skip commands
if (text.startsWith('/')) return;
const telegramId = ctx.from.id.toString();
const response = await handleMessage(telegramId, text);
ctx.reply(response);
});
// API routes for frontend
app.post('/api/link-telegram', (req, res) => {
const { telegramId, walletAddress } = req.body;
linkTelegramToWallet(telegramId, walletAddress);
res.json({ success: true });
});
app.post('/api/store-signer', (req, res) => {
const { walletAddress, privateKey } = req.body;
storeSigner(walletAddress, privateKey);
res.json({ success: true });
});
app.get('/api/check-signer/:address', (req, res) => {
const exists = hasSigner(req.params.address);
res.json({ exists });
});
// Start server
const PORT = process.env.PORT || 8008;
app.listen(PORT, () => {
console.log(`✅ API server running on port ${PORT}`);
});
// Start bot
bot.launch();
console.log('✅ Telegram bot started');
// Graceful shutdown
process.once('SIGINT', () => bot.stop('SIGINT'));
process.once('SIGTERM', () => bot.stop('SIGTERM'));For additional slash commands like /positions and /orders, see the GitHub repository.
Next, we'll build the frontend registration UI.