Sentimint Backend

Production-ready Node.js/Express backend for crypto trading bot SaaS

JWT Authentication LemonSqueezy Payments Trading Queue WebSocket Affiliate System

Project Structure

sentimint-backend/
├── config/               # Configuration files
│   ├── database.js       # Database connection
│   ├── jwt.js            # JWT configuration
│   └── websocket.js      # WebSocket configuration
├── controllers/          # Route controllers
│   ├── auth.controller.js
│   ├── bot.controller.js
│   ├── payment.controller.js
│   ├── affiliate.controller.js
│   └── user.controller.js
├── models/               # Database models
│   ├── User.js
│   ├── Subscription.js
│   ├── Trade.js
│   ├── Affiliate.js
│   └── Queue.js
├── middleware/           # Custom middleware
│   ├── auth.js
│   ├── error.js
│   └── validation.js
├── routes/               # API routes
│   ├── auth.routes.js
│   ├── bot.routes.js
│   ├── payment.routes.js
│   ├── affiliate.routes.js
│   └── user.routes.js
├── services/             # Business logic
│   ├── auth.service.js
│   ├── bot.service.js
│   ├── payment.service.js
│   ├── affiliate.service.js
│   ├── queue.service.js
│   └── websocket.service.js
├── utils/                # Utility functions
│   ├── logger.js
│   ├── helpers.js
│   └── validators.js
├── queues/               # Bull queue processors
│   └── trading.queue.js
├── app.js                # Express app setup
├── server.js             # Server entry point
└── .env                  # Environment variables

Database Models

User Model

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const UserSchema = new mongoose.Schema({
    email: {
        type: String,
        required: true,
        unique: true,
        trim: true,
        lowercase: true
    },
    password: {
        type: String,
        required: true,
        minlength: 8
    },
    isVerified: {
        type: Boolean,
        default: false
    },
    role: {
        type: String,
        enum: ['user', 'admin'],
        default: 'user'
    },
    tradingEnabled: {
        type: Boolean,
        default: false
    },
    apiKey: {
        type: String,
        default: ''
    },
    apiSecret: {
        type: String,
        default: ''
    },
    affiliateCode: {
        type: String,
        unique: true
    },
    referredBy: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User'
    },
    resetPasswordToken: String,
    resetPasswordExpire: Date,
    emailVerificationToken: String,
    emailVerificationExpire: Date,
    lastLogin: Date,
    loginHistory: [{
        ip: String,
        device: String,
        timestamp: Date
    }]
}, {
    timestamps: true
});

// Hash password before saving
UserSchema.pre('save', async function(next) {
    if (!this.isModified('password')) return next();
    
    const salt = await bcrypt.genSalt(10);
    this.password = await bcrypt.hash(this.password, salt);
    next();
});

// Generate affiliate code if not exists
UserSchema.pre('save', function(next) {
    if (!this.affiliateCode) {
        this.affiliateCode = Math.random().toString(36).substring(2, 8) + 
                            Math.random().toString(36).substring(2, 8);
    }
    next();
});

// Method to compare passwords
UserSchema.methods.matchPassword = async function(enteredPassword) {
    return await bcrypt.compare(enteredPassword, this.password);
};

module.exports = mongoose.model('User', UserSchema);

Subscription Model

const mongoose = require('mongoose');

const SubscriptionSchema = new mongoose.Schema({
    user: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        required: true
    },
    lemonSqueezyId: {
        type: String,
        required: true
    },
    orderId: {
        type: String,
        required: true
    },
    productId: {
        type: String,
        required: true,
        enum: ['basic', 'pro', 'enterprise'] // Corresponds to $99/$299/$999 tiers
    },
    status: {
        type: String,
        enum: ['active', 'expired', 'cancelled', 'pending'],
        default: 'pending'
    },
    currentPeriodEnd: Date,
    renewsAt: Date,
    trialEndsAt: Date,
    isUsageBased: {
        type: Boolean,
        default: false
    },
    subscriptionItemId: String,
    variantId: String,
    paymentMethod: String,
    billingAnchor: Number,
    urls: {
        updatePaymentMethod: String,
        customerPortal: String
    },
    cancelReason: String,
    cancelledAt: Date,
    metadata: mongoose.Schema.Types.Mixed
}, {
    timestamps: true
});

module.exports = mongoose.model('Subscription', SubscriptionSchema);

Trade Model

const mongoose = require('mongoose');

const TradeSchema = new mongoose.Schema({
    user: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        required: true
    },
    botId: {
        type: String,
        required: true
    },
    exchange: {
        type: String,
        required: true,
        enum: ['binance', 'kucoin', 'coinbase', 'kraken']
    },
    pair: {
        type: String,
        required: true
    },
    direction: {
        type: String,
        enum: ['long', 'short'],
        required: true
    },
    entryPrice: {
        type: Number,
        required: true
    },
    exitPrice: {
        type: Number
    },
    amount: {
        type: Number,
        required: true
    },
    leverage: {
        type: Number,
        default: 1
    },
    status: {
        type: String,
        enum: ['pending', 'open', 'closed', 'cancelled', 'failed'],
        default: 'pending'
    },
    pnl: {
        type: Number
    },
    pnlPercentage: {
        type: Number
    },
    fees: {
        type: Number
    },
    strategy: {
        type: String
    },
    indicators: mongoose.Schema.Types.Mixed,
    notes: String,
    closedAt: Date,
    metadata: mongoose.Schema.Types.Mixed
}, {
    timestamps: true
});

module.exports = mongoose.model('Trade', TradeSchema);

Affiliate Model

const mongoose = require('mongoose');

const AffiliateSchema = new mongoose.Schema({
    affiliate: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        required: true
    },
    referredUser: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        required: true
    },
    commissionRate: {
        type: Number,
        default: 0.3 // 30% commission
    },
    commissionAmount: {
        type: Number,
        default: 0
    },
    status: {
        type: String,
        enum: ['pending', 'eligible', 'paid'],
        default: 'pending'
    },
    paymentId: String,
    paidAt: Date,
    subscriptionTier: {
        type: String,
        enum: ['basic', 'pro', 'enterprise']
    },
    metadata: mongoose.Schema.Types.Mixed
}, {
    timestamps: true
});

// Index for faster queries
AffiliateSchema.index({ affiliate: 1, referredUser: 1 }, { unique: true });

module.exports = mongoose.model('Affiliate', AffiliateSchema);

Queue Model

const mongoose = require('mongoose');

const QueueSchema = new mongoose.Schema({
    jobId: {
        type: String,
        required: true,
        unique: true
    },
    type: {
        type: String,
        required: true,
        enum: ['trade', 'analysis', 'alert']
    },
    status: {
        type: String,
        enum: ['queued', 'processing', 'completed', 'failed', 'cancelled'],
        default: 'queued'
    },
    priority: {
        type: Number,
        default: 0
    },
    data: mongoose.Schema.Types.Mixed,
    result: mongoose.Schema.Types.Mixed,
    error: mongoose.Schema.Types.Mixed,
    startedAt: Date,
    completedAt: Date,
    attempts: {
        type: Number,
        default: 0
    },
    maxAttempts: {
        type: Number,
        default: 3
    },
    delay: {
        type: Number,
        default: 0
    },
    timeout: {
        type: Number,
        default: 30000 // 30 seconds
    },
    createdBy: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User'
    }
}, {
    timestamps: true
});

module.exports = mongoose.model('Queue', QueueSchema);

API Endpoints

Authentication

POST /api/v1/auth/register - Register a new user
POST /api/v1/auth/login - Login user
POST /api/v1/auth/forgot-password - Request password reset
PUT /api/v1/auth/reset-password/:token - Reset password
GET /api/v1/auth/verify-email/:token - Verify email
POST /api/v1/auth/resend-verification - Resend verification email
GET /api/v1/auth/me - Get current user
POST /api/v1/auth/refresh-token - Refresh access token
POST /api/v1/auth/logout - Logout user

User

PUT /api/v1/users/me - Update user profile
PUT /api/v1/users/password - Change password
POST /api/v1/users/api-keys - Set exchange API keys
DELETE /api/v1/users/api-keys - Remove API keys
GET /api/v1/users/activity - Get user activity

Subscription & Payments

GET /api/v1/subscriptions/plans - Get available subscription plans
POST /api/v1/subscriptions/checkout - Create checkout session
GET /api/v1/subscriptions/me - Get user's subscription
POST /api/v1/subscriptions/cancel - Request cancellation
POST /api/v1/subscriptions/update-payment-method - Update payment method
POST /api/v1/subscriptions/webhook - LemonSqueezy webhook handler
GET /api/v1/subscriptions/invoices - Get subscription invoices

Trading Bot

GET /api/v1/bot/status - Get bot status
POST /api/v1/bot/start - Start trading bot
POST /api/v1/bot/stop - Stop trading bot
POST /api/v1/bot/strategies - Add/update strategy
GET /api/v1/bot/strategies - Get strategies
DELETE /api/v1/bot/strategies/:id - Delete strategy
POST /api/v1/bot/trade - Execute trade
GET /api/v1/bot/trades - Get trade history
GET /api/v1/bot/performance - Get performance metrics
POST /api/v1/bot/queue - Add to trading queue
GET /api/v1/bot/queue - Get queue status

Affiliate System

GET /api/v1/affiliates/stats - Get affiliate stats
GET /api/v1/affiliates/referrals - Get referral list
GET /api/v1/affiliates/commissions - Get commission history
GET /api/v1/affiliates/link - Get affiliate link
POST /api/v1/affiliates/payout - Request payout
GET /api/v1/affiliates/payouts - Get payout history

Core Implementation

app.js - Express Setup

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
const hpp = require('hpp');
const cookieParser = require('cookie-parser');
const compression = require('compression');
const path = require('path');
const http = require('http');
const socketio = require('socket.io');
const logger = require('./utils/logger');
const errorHandler = require('./middleware/error');
const websocket = require('./services/websocket.service');

// Create express app
const app = express();

// Trust proxy
app.set('trust proxy', true);

// Enable CORS
app.use(cors({
    origin: process.env.FRONTEND_URL,
    credentials: true
}));

// Set security HTTP headers
app.use(helmet());

// Rate limiting
const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 200, // limit each IP to 200 requests per windowMs
    message: 'Too many requests from this IP, please try again later'
});
app.use('/api', limiter);

// Body parser, reading data from body into req.body
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
app.use(cookieParser());

// Data sanitization against NoSQL query injection
app.use(mongoSanitize());

// Data sanitization against XSS
app.use(xss());

// Prevent parameter pollution
app.use(hpp());

// Compress responses
app.use(compression());

// Static files
app.use(express.static(path.join(__dirname, 'public')));

// API routes
app.use('/api/v1/auth', require('./routes/auth.routes'));
app.use('/api/v1/users', require('./routes/user.routes'));
app.use('/api/v1/subscriptions', require('./routes/payment.routes'));
app.use('/api/v1/bot', require('./routes/bot.routes'));
app.use('/api/v1/affiliates', require('./routes/affiliate.routes'));

// Health check endpoint
app.get('/health', (req, res) => res.status(200).send('OK'));

// Handle 404
app.all('*', (req, res, next) => {
    res.status(404).json({
        status: 'fail',
        message: `Can't find ${req.originalUrl} on this server!`
    });
});

// Error handling middleware
app.use(errorHandler);

// Create HTTP server
const server = http.createServer(app);

// Set up Socket.io
const io = socketio(server, {
    cors: {
        origin: process.env.FRONTEND_URL,
        methods: ['GET', 'POST'],
        credentials: true
    }
});

// Initialize WebSocket service
websocket.initialize(io);

module.exports = server;

server.js - Entry Point

const app = require('./app');
const mongoose = require('mongoose');
const config = require('./config/database');
const logger = require('./utils/logger');
const queueService = require('./services/queue.service');

// Handle uncaught exceptions
process.on('uncaughtException', err => {
    logger.error('UNCAUGHT EXCEPTION! 💥 Shutting down...');
    logger.error(err.name, err.message);
    process.exit(1);
});

// Connect to database
mongoose.connect(config.uri, config.options)
    .then(() => logger.info('DB connection successful!'))
    .catch(err => {
        logger.error('DB connection failed!');
        logger.error(err);
        process.exit(1);
    });

// Start server
const port = process.env.PORT || 3000;
const server = app.listen(port, () => {
    logger.info(`Server running on port ${port}...`);
});

// Initialize queues
queueService.initialize();

// Handle unhandled promise rejections
process.on('unhandledRejection', err => {
    logger.error('UNHANDLED REJECTION! 💥 Shutting down...');
    logger.error(err.name, err.message);
    server.close(() => {
        process.exit(1);
    });
});

// Handle SIGTERM
process.on('SIGTERM', () => {
    logger.info('👋 SIGTERM RECEIVED. Shutting down gracefully');
    server.close(() => {
        logger.info('💥 Process terminated!');
    });
});

WebSocket Service

const logger = require('../utils/logger');
const jwt = require('jsonwebtoken');
const User = require('../models/User');

class WebSocketService {
    constructor() {
        this.io = null;
        this.connectedUsers = new Map();
    }

    initialize(io) {
        this.io = io;
        
        // Authentication middleware
        io.use(async (socket, next) => {
            try {
                const token = socket.handshake.auth.token;
                
                if (!token) {
                    return next(new Error('Authentication error: Token not provided'));
                }

                const decoded = jwt.verify(token, process.env.JWT_SECRET);
                const user = await User.findById(decoded.id);

                if (!user) {
                    return next(new Error('Authentication error: User not found'));
                }

                socket.user = user;
                next();
            } catch (err) {
                next(new Error('Authentication error: Invalid token'));
            }
        });

        // Connection handler
        io.on('connection', (socket) => {
            logger.info(`New WebSocket connection: ${socket.id}`);
            
            // Store user connection
            if (socket.user) {
                this.connectedUsers.set(socket.user._id.toString(), socket);
                logger.info(`User ${socket.user.email} connected via WebSocket`);
            }

            // Trade updates subscription
            socket.on('subscribe:trades', (data) => {
                if (socket.user) {
                    socket.join(`user:${socket.user._id}:trades`);
                    logger.info(`User ${socket.user.email} subscribed to trade updates`);
                }
            });

            // Bot status subscription
            socket.on('subscribe:bot-status', (data) => {
                if (socket.user) {
                    socket.join(`user:${socket.user._id}:bot-status`);
                    logger.info(`User ${socket.user.email} subscribed to bot status updates`);
                }
            });

            // Queue updates subscription
            socket.on('subscribe:queue', (data) => {
                if (socket.user) {
                    socket.join(`user:${socket.user._id}:queue`);
                    logger.info(`User ${socket.user.email} subscribed to queue updates`);
                }
            });

            // Disconnection handler
            socket.on('disconnect', () => {
                logger.info(`WebSocket disconnected: ${socket.id}`);
                if (socket.user) {
                    this.connectedUsers.delete(socket.user._id.toString());
                }
            });
        });
    }

    // Send trade update to specific user
    sendTradeUpdate(userId, trade) {
        if (this.io) {
            this.io.to(`user:${userId}:trades`).emit('trade:update', trade);
        }
    }

    // Send bot status update to specific user
    sendBotStatusUpdate(userId, status) {
        if (this.io) {
            this.io.to(`user:${userId}:bot-status`).emit('bot:status', status);
        }
    }

    // Send queue update to specific user
    sendQueueUpdate(userId, queueItem) {
        if (this.io) {
            this.io.to(`user:${userId}:queue`).emit('queue:update', queueItem);
        }
    }

    // Send notification to specific user
    sendNotification(userId, notification) {
        if (this.io) {
            const socket = this.connectedUsers.get(userId.toString());
            if (socket) {
                socket.emit('notification', notification);
            }
        }
    }
}

module.exports = new WebSocketService();

Trading Queue Service

const Queue = require('bull');
const mongoose = require('mongoose');
const logger = require('../utils/logger');
const Trade = require('../models/Trade');
const QueueModel = require('../models/Queue');
const websocket = require('./websocket.service');
const { executeTrade } = require('./bot.service');

class QueueService {
    constructor() {
        this.tradingQueue = null;
    }

    initialize() {
        // Create trading queue
        this.tradingQueue = new Queue('trading', {
            redis: {
                host: process.env.REDIS_HOST,
                port: process.env.REDIS_PORT,
                password: process.env.REDIS_PASSWORD
            },
            defaultJobOptions: {
                removeOnComplete: true,
                removeOnFail: true,
                attempts: 3,
                backoff: {
                    type: 'exponential',
                    delay: 5000
                }
            }
        });

        // Process queue jobs
        this.tradingQueue.process('execute-trade', async (job) => {
            const { userId, tradeData } = job.data;
            
            try {
                // Update queue status in DB
                await QueueModel.findOneAndUpdate(
                    { jobId: job.id.toString() },
                    { status: 'processing', startedAt: new Date() },
                    { new: true }
                );

                // Execute the trade
                const trade = await executeTrade(userId, tradeData);

                // Update queue status
                await QueueModel.findOneAndUpdate(
                    { jobId: job.id.toString() },
                    { status: 'completed', completedAt: new Date(), result: trade },
                    { new: true }
                );

                // Send WebSocket update
                websocket.sendQueueUpdate(userId, {
                    jobId: job.id.toString(),
                    status: 'completed',
                    trade
                });

                return trade;
            } catch (err) {
                // Update queue status
                await QueueModel.findOneAndUpdate(
                    { jobId: job.id.toString() },
                    { 
                        status: 'failed', 
                        completedAt: new Date(), 
                        error: err.message,
                        attempts: job.attemptsMade
                    },
                    { new: true }
                );

                // Send WebSocket update
                websocket.sendQueueUpdate(userId, {
                    jobId: job.id.toString(),
                    status: 'failed',
                    error: err.message
                });

                throw err;
            }
        });

        // Event listeners
        this.tradingQueue.on('completed', (job, result) => {
            logger.info(`Job ${job.id} completed with result:`, result);
        });

        this.tradingQueue.on('failed', (job, err) => {
            logger.error(`Job ${job.id} failed with error:`, err);
        });

        this.tradingQueue.on('error', (err) => {
            logger.error('Queue error:', err);
        });
    }

    async addTradeToQueue(userId, tradeData) {
        try {
            // Add job to queue
            const job = await this.tradingQueue.add('execute-trade', { userId, tradeData }, {
                priority: tradeData.priority || 0,
                delay: tradeData.delay || 0
            });

            // Save to database
            const queueItem = new QueueModel({
                jobId: job.id.toString(),
                type: 'trade',
                status: 'queued',
                priority: tradeData.priority || 0,
                data: tradeData,
                createdBy: userId
            });

            await queueItem.save();

            // Send WebSocket update
            websocket.sendQueueUpdate(userId, {
                jobId: job.id.toString(),
                status: 'queued',
                position: await job.getState()
            });

            return queueItem;
        } catch (err) {
            logger.error('Error adding to queue:', err);
            throw err;
        }
    }

    async getQueueStatus(userId) {
        try {
            const jobs = await this.tradingQueue.getJobs(['waiting', 'active', 'completed', 'failed']);
            
            // Filter jobs for this user
            const userJobs = jobs.filter(job => job.data.userId.toString() === userId.toString());
            
            // Get from database for more details
            const queueItems = await QueueModel.find({
                createdBy: userId,
                status: { $in: ['queued', 'processing'] }
            }).sort('-createdAt');

            return {
                waiting: await this.tradingQueue.getWaitingCount(),
                active: await this.tradingQueue.getActiveCount(),
                completed: await this.tradingQueue.getCompletedCount(),
                failed: await this.tradingQueue.getFailedCount(),
                userJobs: queueItems
            };
        } catch (err) {
            logger.error('Error getting queue status:', err);
            throw err;
        }
    }
}

module.exports = new QueueService();

LemonSqueezy Payment Service

const axios = require('axios');
const crypto = require('crypto');
const logger = require('../utils/logger');
const Subscription = require('../models/Subscription');
const User = require('../models/User');
const Affiliate = require('../models/Affiliate');
const websocket = require('./websocket.service');

class PaymentService {
    constructor() {
        this.apiUrl = 'https://api.lemonsqueezy.com/v1';
        this.headers = {
            'Accept': 'application/vnd.api+json',
            'Content-Type': 'application/vnd.api+json',
            'Authorization': `Bearer ${process.env.LEMON_SQUEEZY_API_KEY}`
        };
    }

    async createCheckout(user, variantId, affiliateCode = null) {
        try {
            // Create checkout URL
            const response = await axios.post(`${this.apiUrl}/checkouts`, {
                data: {
                    type: 'checkouts',
                    attributes: {
                        checkout_data: {
                            email: user.email,
                            custom: {
                                user_id: user._id.toString(),
                                affiliate_code: affiliateCode
                            }
                        },
                        product_options: {
                            enabled_variants: [variantId],
                            redirect_url: `${process.env.FRONTEND_URL}/dashboard`,
                            receipt_button_text: 'Go to Dashboard',
                            receipt_thank_you_note: 'Thank you for subscribing to Sentimint!'
                        },
                        expires_at: null,
                        test_mode: process.env.NODE_ENV !== 'production'
                    },
                    relationships: {
                        store: {
                            data: {
                                type: 'stores',
                                id: process.env.LEMON_SQUEEZY_STORE_ID
                            }
                        },
                        variant: {
                            data: {
                                type: 'variants',
                                id: variantId
                            }
                        }
                    }
                }
            }, { headers: this.headers });

            return response.data.data.attributes.url;
        } catch (err) {
            logger.error('Error creating checkout:', err.response?.data || err.message);
            throw err;
        }
    }

    async handleWebhook(payload, signature) {
        try {
            // Verify webhook signature
            const hmac = crypto.createHmac('sha256', process.env.LEMON_SQUEEZY_WEBHOOK_SECRET);
            const digest = hmac.update(JSON.stringify(payload)).digest('hex');
            
            if (signature !== digest) {
                throw new Error('Invalid webhook signature');
            }

            const eventName = payload.meta.event_name;
            const eventData = payload.data;
            
            logger.info(`Processing LemonSqueezy webhook: ${eventName}`);

            // Handle different event types
            switch (eventName) {
                case 'order_created':
                    await this.handleOrderCreated(eventData);
                    break;
                
                case 'subscription_created':
                    await this.handleSubscriptionCreated(eventData);
                    break;
                
                case 'subscription_updated':
                    await this.handleSubscriptionUpdated(eventData);
                    break;
                
                case 'subscription_cancelled':
                    await this.handleSubscriptionCancelled(eventData);
                    break;
                
                case 'subscription_expired':
                    await this.handleSubscriptionExpired(eventData);
                    break;
                
                case 'subscription_resumed':
                    await this.handleSubscriptionResumed(eventData);
                    break;
                
                case 'subscription_payment_success':
                    await this.handlePaymentSuccess(eventData);
                    break;
                
                case 'subscription_payment_failed':
                    await this.handlePaymentFailed(eventData);
                    break;
                
                case 'subscription_payment_recovered':
                    await this.handlePaymentRecovered(eventData);
                    break;
                
                default:
                    logger.info(`Unhandled webhook event: ${eventName}`);
            }

            return { success: true };
        } catch (err) {
            logger.error('Error processing webhook:', err);
            throw err;
        }
    }

    async handleOrderCreated(data) {
        const customData = data.attributes.custom_data || {};
        const userId = customData.user_id;
        const affiliateCode = customData.affiliate_code;
        
        if (!userId) return;

        // Check if this is a subscription purchase
        const includedSubscriptions = payload.included?.filter(item => item.type === 'subscriptions');
        if (!includedSubscriptions || includedSubscriptions.length === 0) return;

        // Handle affiliate commission if applicable
        if (affiliateCode) {
            const affiliateUser = await User.findOne({ affiliateCode });
            if (affiliateUser) {
                const tier = this.getTierFromVariantId(data.attributes.variant_id);
                
                const affiliateRecord = new Affiliate({
                    affiliate: affiliateUser._id,
                    referredUser: userId,
                    subscriptionTier: tier,
                    status: 'pending'
                });

                await affiliateRecord.save();
                
                // Notify affiliate
                websocket.sendNotification(affiliateUser._id, {
                    type: 'affiliate',
                    message: `New referral: ${data.attributes.user_email}`
                });
            }
        }
    }

    async handleSubscriptionCreated(data) {
        const userId = data.attributes.user_id;
        if (!userId) return;

        const user = await User.findById(userId);
        if (!user) return;

        const variantId = data.attributes.variant_id;
        const tier = this.getTierFromVariantId(variantId);

        // Create or update subscription
        const subscription = await Subscription.findOneAndUpdate(
            { user: userId },
            {
                lemonSqueezyId: data.id,
                orderId: data.attributes.order_id,
                productId: tier,
                status: 'active',
                currentPeriodEnd: new Date(data.attributes.renews_at),
                renewsAt: new Date(data.attributes.renews_at),
                urls: {
                    updatePaymentMethod: data.attributes.urls.update_payment_method,
                    customerPortal: data.attributes.urls.customer_portal
                },
                paymentMethod: data.attributes.payment_method,
                billingAnchor: data.attributes.billing_anchor
            },
            { upsert: true, new: true }
        );

        // Enable trading for user
        user.tradingEnabled = true;
        await user.save();

        // Send notification
        websocket.sendNotification(userId, {
            type: 'subscription',
            message: `Your ${tier} subscription is now active!`
        });

        return subscription;
    }

    async handleSubscriptionUpdated(data) {
        const subscription = await Subscription.findOne({ lemonSqueezyId: data.id });
        if (!subscription) return;

        // Update subscription details
        subscription.currentPeriodEnd = new Date(data.attributes.renews_at);
        subscription.renewsAt = new Date(data.attributes.renews_at);
        subscription.paymentMethod = data.attributes.payment_method;
        subscription.urls.updatePaymentMethod = data.attributes.urls.update_payment_method;
        subscription.urls.customerPortal = data.attributes.urls.customer_portal;

        await subscription.save();

        // Send notification
        websocket.sendNotification(subscription.user, {
            type: 'subscription',
            message: 'Your subscription has been updated'
        });
    }

    async handleSubscriptionCancelled(data) {
        const subscription = await Subscription.findOne({ lemonSqueezyId: data.id });
        if (!subscription) return;

        // Update subscription status
        subscription.status = 'cancelled';
        subscription.cancelReason = data.attributes.cancellation_reason;
        subscription.cancelledAt = new Date(data.attributes.ends_at);

        await subscription.save();

        // Disable trading for user
        const user = await User.findById(subscription.user);
        if (user) {
            user.tradingEnabled = false;
            await user.save();
        }

        // Send notification
        websocket.sendNotification(subscription.user, {
            type: 'subscription',
            message: 'Your subscription has been cancelled'
        });
    }

    async handlePaymentSuccess(data) {
        const subscription = await Subscription.findOne({ lemonSqueezyId: data.attributes.subscription_id });
        if (!subscription) return;

        // Update subscription renewal date
        subscription.currentPeriodEnd = new Date(data.attributes.renews_at);
        subscription.renewsAt = new Date(data.attributes.renews_at);
        await subscription.save();

        // Handle affiliate commission
        const affiliateRecord = await Affiliate.findOne({
            referredUser: subscription.user,
            status: 'pending'
        });

        if (affiliateRecord) {
            const tier = subscription.productId;
            let commissionAmount = 0;

            // Calculate commission based on tier
            if (tier === 'basic') commissionAmount = 99 * 0.3; // 30% of $99
            if (tier === 'pro') commissionAmount = 299 * 0.3; // 30% of $299
            if (tier === 'enterprise') commissionAmount = 999 * 0.3; // 30% of $999

            affiliateRecord.commissionAmount = commissionAmount;
            affiliateRecord.status = 'eligible';
            await affiliateRecord.save();

            // Notify affiliate
            websocket.sendNotification(affiliateRecord.affiliate, {
                type: 'affiliate',
                message: `You've earned $${commissionAmount.toFixed(2)} from a referral!`
            });
        }

        // Send notification to user
        websocket.sendNotification(subscription.user, {
            type: 'payment',
            message: 'Your subscription payment was successful'
        });
    }

    getTierFromVariantId(variantId) {
        // Map LemonSqueezy variant IDs to subscription tiers
        const variantMap = {
            [process.env.LEMON_SQUEEZY_BASIC_VARIANT_ID]: 'basic',
            [process.env.LEMON_SQUEEZY_PRO_VARIANT_ID]: 'pro',
            [process.env.LEMON_SQUEEZY_ENTERPRISE_VARIANT_ID]: 'enterprise'
        };

        return variantMap[variantId] || 'basic';
    }
}

module.exports = new PaymentService();

Environment Variables

# Server
NODE_ENV=development
PORT=3000
FRONTEND_URL=http://localhost:3000

# Database
MONGODB_URI=mongodb://localhost:27017/sentimint
MONGODB_OPTIONS={}

# JWT
JWT_SECRET=your_jwt_secret
JWT_EXPIRE=30d
JWT_COOKIE_EXPIRE=30

# LemonSqueezy
LEMON_SQUEEZY_API_KEY=your_api_key
LEMON_SQUEEZY_STORE_ID=your_store_id
LEMON_SQUEEZY_WEBHOOK_SECRET=your_webhook_secret
LEMON_SQUEEZY_BASIC_VARIANT_ID=your_basic_variant_id
LEMON_SQUEEZY_PRO_VARIANT_ID=your_pro_variant_id
LEMON_SQUEEZY_ENTERPRISE_VARIANT_ID=your_enterprise_variant_id

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=

# Email (optional)
SMTP_HOST=
SMTP_PORT=
SMTP_USERNAME=
SMTP_PASSWORD=
EMAIL_FROM=

Installation & Setup

1. Prerequisites

  • Node.js v16+
  • MongoDB
  • Redis
  • LemonSqueezy account with configured products

2. Installation

# Clone the repository
git clone https://github.com/your-repo/sentimint-backend.git
cd sentimint-backend

# Install dependencies
npm install

# Create .env file and configure environment variables
cp .env.example .env
nano .env

# Start the server
npm run dev

3. Production Deployment

# Build for production
npm run build

# Start in production mode
npm start

# Using PM2 (recommended)
npm install -g pm2
pm2 start dist/server.js --name sentimint-backend

Connecting with React Frontend

API Client Setup

// src/api/client.js
import axios from 'axios';

const apiClient = axios.create({
    baseURL: process.env.REACT_APP_API_URL || 'http://localhost:3000/api/v1',
    withCredentials: true
});

// Add request interceptor for JWT
apiClient.interceptors.request.use((config) => {
    const token = localStorage.getItem('token');
    if (token) {
        config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
});

// Add response interceptor for error handling
apiClient.interceptors.response.use(
    (response) => response,
    (error) => {
        if (error.response?.status === 401) {
            // Handle unauthorized (token expired)
            localStorage.removeItem('token');
            window.location.href = '/login';
        }
        return Promise.reject(error);
    }
);

export default apiClient;

WebSocket Setup

// src/api/websocket.js
import io from 'socket.io-client';

let socket;

export const connectWebSocket = (token) => {
    socket = io(process.env.REACT_APP_API_URL || 'http://localhost:3000', {
        auth: { token },
        transports: ['websocket']
    });

    return socket;
};

export const disconnectWebSocket = () => {
    if (socket) {
        socket.disconnect();
    }
};

export const getSocket = () => socket;

Example API Calls

// Example API calls from React frontend
import apiClient from './client';

// User registration
export const register = async (userData) => {
    const response = await apiClient.post('/auth/register', userData);
    return response.data;
};

// User login
export const login = async (credentials) => {
    const response = await apiClient.post('/auth/login', credentials);
    return response.data;
};

// Get user profile
export const getMe = async () => {
    const response = await apiClient.get('/users/me');
    return response.data;
};

// Start trading bot
export const startBot = async (strategy) => {
    const response = await apiClient.post('/bot/start', { strategy });
    return response.data;
};

// Get trade history
export const getTrades = async (params) => {
    const response = await apiClient.get('/bot/trades', { params });
    return response.data;
};

Made with DeepSite LogoDeepSite - 🧬 Remix