Spaces:
Running
Running
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
| <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> |