backend / index.html
Scorpiotur's picture
Create a complete Node.js/Express backend for Sentimint crypto trading bot SaaS. Requirements: 1. User authentication (JWT) 2. Stripe integration for payments ($99/$299/$999 tiers) 3. Trading bot integration with queue system 4. Real-time WebSocket for live trades 5. Complete API for frontend Must include: - Database models (User, Subscription, Trade) - API endpoints for auth, trading, dashboard, billing - Stripe webhook handling for subscriptions - WebSocket server for real-time updates - Security middleware - Ready to connect with existing React frontend Skip affiliate system for now - will add later. Make it production-ready with error handling. ({/* Affiliate Program - Coming Soon! */} <Badge>Launching July 2025</Badge>) - Follow Up Deployment
b6965ce verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sentimint Backend Documentation</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.code-block {
background-color: #1e293b;
color: #f8fafc;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
font-family: 'Courier New', Courier, monospace;
font-size: 0.875rem;
line-height: 1.5;
margin: 1rem 0;
}
.endpoint {
background-color: #0f172a;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
margin: 0.5rem 0;
}
.method-get { color: #4ade80; }
.method-post { color: #60a5fa; }
.method-put { color: #fbbf24; }
.method-delete { color: #f87171; }
</style>
</head>
<body class="bg-slate-900 text-slate-100">
<div class="container mx-auto px-4 py-8">
<header class="mb-12 text-center">
<h1 class="text-4xl font-bold text-emerald-400 mb-2">Sentimint Backend</h1>
<p class="text-xl text-slate-300">Production-ready Node.js/Express backend for crypto trading bot SaaS</p>
<div class="mt-6 flex justify-center space-x-4">
<span class="px-4 py-2 bg-emerald-800 rounded-full text-sm">JWT Authentication</span>
<span class="px-4 py-2 bg-blue-800 rounded-full text-sm">LemonSqueezy Payments</span>
<span class="px-4 py-2 bg-amber-800 rounded-full text-sm">Trading Queue</span>
<span class="px-4 py-2 bg-purple-800 rounded-full text-sm">WebSocket</span>
<span class="px-4 py-2 bg-pink-800 rounded-full text-sm">Affiliate System</span>
</div>
</header>
<section class="mb-12">
<h2 class="text-2xl font-semibold text-emerald-400 mb-4">Project Structure</h2>
<div class="code-block">
<pre>
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</pre>
</div>
</section>
<section class="mb-12">
<h2 class="text-2xl font-semibold text-emerald-400 mb-4">Database Models</h2>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">User Model</h3>
<div class="code-block">
<pre>
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);</pre>
</div>
</div>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">Subscription Model</h3>
<div class="code-block">
<pre>
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);</pre>
</div>
</div>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">Trade Model</h3>
<div class="code-block">
<pre>
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);</pre>
</div>
</div>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">Affiliate Model</h3>
<div class="code-block">
<pre>
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);</pre>
</div>
</div>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">Queue Model</h3>
<div class="code-block">
<pre>
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);</pre>
</div>
</div>
</section>
<section class="mb-12">
<h2 class="text-2xl font-semibold text-emerald-400 mb-4">API Endpoints</h2>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">Authentication</h3>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/auth/register - Register a new user
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/auth/login - Login user
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/auth/forgot-password - Request password reset
</div>
<div class="endpoint">
<span class="method-put">PUT</span> /api/v1/auth/reset-password/:token - Reset password
</div>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/auth/verify-email/:token - Verify email
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/auth/resend-verification - Resend verification email
</div>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/auth/me - Get current user
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/auth/refresh-token - Refresh access token
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/auth/logout - Logout user
</div>
</div>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">User</h3>
<div class="endpoint">
<span class="method-put">PUT</span> /api/v1/users/me - Update user profile
</div>
<div class="endpoint">
<span class="method-put">PUT</span> /api/v1/users/password - Change password
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/users/api-keys - Set exchange API keys
</div>
<div class="endpoint">
<span class="method-delete">DELETE</span> /api/v1/users/api-keys - Remove API keys
</div>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/users/activity - Get user activity
</div>
</div>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">Subscription & Payments</h3>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/subscriptions/plans - Get available subscription plans
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/subscriptions/checkout - Create checkout session
</div>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/subscriptions/me - Get user's subscription
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/subscriptions/cancel - Request cancellation
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/subscriptions/update-payment-method - Update payment method
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/subscriptions/webhook - LemonSqueezy webhook handler
</div>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/subscriptions/invoices - Get subscription invoices
</div>
</div>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">Trading Bot</h3>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/bot/status - Get bot status
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/bot/start - Start trading bot
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/bot/stop - Stop trading bot
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/bot/strategies - Add/update strategy
</div>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/bot/strategies - Get strategies
</div>
<div class="endpoint">
<span class="method-delete">DELETE</span> /api/v1/bot/strategies/:id - Delete strategy
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/bot/trade - Execute trade
</div>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/bot/trades - Get trade history
</div>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/bot/performance - Get performance metrics
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/bot/queue - Add to trading queue
</div>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/bot/queue - Get queue status
</div>
</div>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">Affiliate System</h3>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/affiliates/stats - Get affiliate stats
</div>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/affiliates/referrals - Get referral list
</div>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/affiliates/commissions - Get commission history
</div>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/affiliates/link - Get affiliate link
</div>
<div class="endpoint">
<span class="method-post">POST</span> /api/v1/affiliates/payout - Request payout
</div>
<div class="endpoint">
<span class="method-get">GET</span> /api/v1/affiliates/payouts - Get payout history
</div>
</div>
</section>
<section class="mb-12">
<h2 class="text-2xl font-semibold text-emerald-400 mb-4">Core Implementation</h2>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">app.js - Express Setup</h3>
<div class="code-block">
<pre>
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;</pre>
</div>
</div>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">server.js - Entry Point</h3>
<div class="code-block">
<pre>
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!');
});
});</pre>
</div>
</div>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">WebSocket Service</h3>
<div class="code-block">
<pre>
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();</pre>
</div>
</div>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">Trading Queue Service</h3>
<div class="code-block">
<pre>
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();</pre>
</div>
</div>
<div class="mb-8">
<h3 class="text-xl font-medium text-blue-300 mb-2">LemonSqueezy Payment Service</h3>
<div class="code-block">
<pre>
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();</pre>
</div>
</div>
</section>
<section class="mb-12">
<h2 class="text-2xl font-semibold text-emerald-400 mb-4">Environment Variables</h2>
<div class="code-block">
<pre>
# 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=</pre>
</div>
</section>
<section class="mb-12">
<h2 class="text-2xl font-semibold text-emerald-400 mb-4">Installation & Setup</h2>
<div class="mb-6">
<h3 class="text-xl font-medium text-blue-300 mb-2">1. Prerequisites</h3>
<ul class="list-disc pl-6 space-y-2">
<li>Node.js v16+</li>
<li>MongoDB</li>
<li>Redis</li>
<li>LemonSqueezy account with configured products</li>
</ul>
</div>
<div class="mb-6">
<h3 class="text-xl font-medium text-blue-300 mb-2">2. Installation</h3>
<div class="code-block">
<pre>
# 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</pre>
</div>
</div>
<div class="mb-6">
<h3 class="text-xl font-medium text-blue-300 mb-2">3. Production Deployment</h3>
<div class="code-block">
<pre>
# 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</pre>
</div>
</div>
</section>
<section class="mb-12">
<h2 class="text-2xl font-semibold text-emerald-400 mb-4">Connecting with React Frontend</h2>
<div class="mb-6">
<h3 class="text-xl font-medium text-blue-300 mb-2">API Client Setup</h3>
<div class="code-block">
<pre>
// 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;</pre>
</div>
</div>
<div class="mb-6">
<h3 class="text-xl font-medium text-blue-300 mb-2">WebSocket Setup</h3>
<div class="code-block">
<pre>
// 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;</pre>
</div>
</div>
<div class="mb-6">
<h3 class="text-xl font-medium text-blue-300 mb-2">Example API Calls</h3>
<div class="code-block">
<pre>
// 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;
};</pre>
</div>
</div>
</section>
<footer class="mt-12 pt-8 border-t border-slate-700 text-center text-slate-400">
<p>Sentimint Crypto Trading Bot Backend - Production Ready</p>
<p class="mt-2">Designed for seamless integration with React frontend</p>
<div class="mt-4 flex justify-center space-x-4">
<a href="#" class="text-emerald-400 hover:text-emerald-300">
<i class="fab fa-github"></i> GitHub
</a>
<a href="#" class="text-emerald-400 hover:text-emerald-300">
<i class="fas fa-book"></i> Documentation
</a>
<a href="#" class="text-emerald-400 hover:text-emerald-300">
<i class="fas fa-code"></i> API Reference
</a>
</div>
</footer>
</div>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Scorpiotur/backend" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>