Bot de cualificación y onboarding de leads para SaaS B2B · grammY · Node.js · Railway
# leadnest-bot/ — grammY + Node.js 20 leadnest-bot/ ├── bot.js # Entry point: init + webhooks ├── handlers/ │ ├── commands.js # /start, /help, /precios, /contacto │ ├── callbacks.js # Inline keyboard actions │ ├── conversations.js# Flujos multi-paso (demo) │ └── middleware.js # Rate limit, logging, auth admin ├── keyboards/ │ ├── menu.js # Teclado menú principal │ ├── precios.js # Selector de plan │ └── faq.js # Árbol de FAQs ├── services/ │ ├── notify.js # Notif. Slack webhook al equipo ventas │ └── leads.js # Persistencia leads (JSON/Supabase) ├── .env # BOT_TOKEN, WEBHOOK_URL, ADMIN_ID └── package.json # grammy, dotenv, express
import { Bot, session, webhookCallback } from 'grammy'; import { conversations, createConversation } from '@grammyjs/conversations'; import express from 'express'; import 'dotenv/config'; import { registerCommands } from './handlers/commands.js'; import { registerCallbacks } from './handlers/callbacks.js'; import { demoFlow } from './handlers/conversations.js'; import { rateLimiter, logger } from './handlers/middleware.js'; const bot = new Bot(process.env.BOT_TOKEN); // Middleware global bot.use(session({ initial: () => ({ step: 'idle', demoData: {} }) })); bot.use(conversations()); bot.use(createConversation(demoFlow, 'demo')); bot.use(rateLimiter({ max: 5, windowMs: 10000 })); bot.use(logger); // Registrar handlers registerCommands(bot); registerCallbacks(bot); // Webhook para producción (Railway) const app = express(); app.use(express.json()); app.post(`/bot${process.env.BOT_TOKEN}`, webhookCallback(bot, 'express')); app.listen(process.env.PORT || 3000, async () => { await bot.api.setWebhook( `${process.env.WEBHOOK_URL}/bot${process.env.BOT_TOKEN}` ); console.log('✅ LeadNest Bot activo (webhook)'); });
import { notifySlack } from '../services/notify.js'; import { saveLead } from '../services/leads.js'; export async function demoFlow(conversation, ctx) { await ctx.reply('👋 ¡Empecemos! ¿Cómo te llamas?'); const name = (await conversation.waitFor('message:text')) .message.text; await ctx.reply(`Encantado, ${name}! ¿En qué empresa trabajas?`); const company = (await conversation.waitFor('message:text')) .message.text; await ctx.reply('📧 ¿Tu email de trabajo?'); const email = (await conversation.waitFor('message:text')) .message.text; await ctx.reply('¿Qué plan te interesa?', { reply_markup: { inline_keyboard: [[ { text: '🚀 Pro (49€)', callback_data: 'plan_pro' }, { text: '🏢 Agency (199€)', callback_data: 'plan_agency' }, ],[ { text: '🆓 Primero pruebo Free', callback_data: 'plan_free' } ]] } }); const { data: plan } = (await conversation .waitForCallbackQuery(['plan_pro', 'plan_agency', 'plan_free'])) .callbackQuery; const lead = { name, company, email, plan, tgId: ctx.from.id, ts: new Date().toISOString() }; await Promise.all([saveLead(lead), notifySlack(lead)]); await ctx.answerCallbackQuery(); await ctx.editMessageText( `✅ *¡Listo, ${name}!*\n\nTu solicitud de demo está registrada. ` + `Nuestro equipo se pondrá en contacto en <24h.\n\n` + `📬 Confirmación enviada a: ${email}`, { parse_mode: 'HTML' } ); }
| Comando | Descripción | Tipo | Handler |
|---|---|---|---|
| /start | Bienvenida + menú principal inline | público | commands.js |
| /demo | Inicia flujo de solicitud de demo (4 pasos) | conversación | conversations.js |
| /precios | Muestra tabla de planes con botones de acción | público | commands.js |
| /faq | Árbol de preguntas frecuentes navegable | público | keyboards/faq.js |
| /contacto | Datos del equipo + enlace directo Calendly | público | commands.js |
| /admin | Panel admin: ver leads, estadísticas | admin only | middleware.js |