diff --git a/PRICING_SYSTEM_COMPLETE.md b/PRICING_SYSTEM_COMPLETE.md new file mode 100644 index 0000000..0a2115c --- /dev/null +++ b/PRICING_SYSTEM_COMPLETE.md @@ -0,0 +1,395 @@ +# TurboTrades Pricing & Payout System + +## šŸ“Š System Overview + +TurboTrades has **two separate pricing systems** for different purposes: + +### 1. **Marketplace** (User-to-User) +- Users list items at their own prices +- Other users buy from listings +- Site takes a commission (configurable in admin panel) +- Prices set by sellers + +### 2. **Instant Sell** (User-to-Site) +- Site **buys items directly** from users +- Instant payout to user balance +- Price = Market Price Ɨ Payout Rate (e.g., 60%) +- Admin configures payout percentage + +--- + +## šŸ—„ļø Database Structure + +### MarketPrice Collection (Reference Database) +- **Purpose**: Reference prices for ALL Steam market items +- **Updated**: Every 1 hour automatically +- **Source**: SteamAPIs.com market data +- **Used for**: Instant sell page pricing +- **Contents**: ~30,000+ items (CS2 + Rust) + +### Item Collection (Marketplace Listings) +- **Purpose**: User-listed items for sale +- **Updated**: Every 1 hour (optional, for listed items) +- **Source**: User-set prices or suggested from MarketPrice +- **Used for**: Marketplace page + +--- + +## āš™ļø How Instant Sell Works + +### User Experience: +1. User goes to `/sell` page +2. Selects items from their Steam inventory +3. Sees **instant buy price** = Market Price Ɨ Payout Rate +4. Accepts offer +5. Site buys items, credits balance immediately + +### Backend Flow: +1. **Fetch Inventory** (`GET /api/inventory/steam`) + - Gets user's Steam inventory + - Looks up each item in `MarketPrice` database + - Applies payout rate (e.g., 60%) + - Returns items with `marketPrice` (already discounted) + +2. **Create Trade** (`POST /api/inventory/trade`) + - Validates items and prices + - Creates trade offer via Steam bot + - User accepts in Steam + - Balance credited on completion + +### Example Calculation: +``` +Market Price (MarketPrice DB): $10.00 +Payout Rate (Admin Config): 60% +User Receives: $6.00 +``` + +--- + +## šŸ”„ Automatic Price Updates + +### On Server Startup: +```javascript +// Runs immediately when server starts +pricingService.updateAllPrices() + → Updates MarketPrice database (CS2 + Rust) + → Updates Item prices (marketplace listings) +``` + +### Every Hour: +```javascript +// Scheduled via setInterval (60 minutes) +pricingService.scheduleUpdates(60 * 60 * 1000) + → updateMarketPriceDatabase('cs2') // ~20,000 items + → updateMarketPriceDatabase('rust') // ~10,000 items + → updateDatabasePrices('cs2') // Marketplace items + → updateDatabasePrices('rust') // Marketplace items +``` + +### What Gets Updated: +- āœ… **MarketPrice**: ALL Steam market items +- āœ… **Item**: Only items listed on marketplace +- ā±ļø **Duration**: ~2-5 minutes per update +- šŸ”‘ **Requires**: `STEAM_APIS_KEY` in `.env` + +--- + +## šŸŽ›ļø Admin Configuration + +### Instant Sell Settings + +**Endpoint:** `PATCH /api/admin/config/instantsell` + +**Settings Available:** +```json +{ + "enabled": true, // Enable/disable instant sell + "payoutRate": 0.6, // 60% default payout + "minItemValue": 0.1, // Min $0.10 + "maxItemValue": 10000, // Max $10,000 + "cs2": { + "enabled": true, + "payoutRate": 0.65 // CS2-specific override (65%) + }, + "rust": { + "enabled": true, + "payoutRate": 0.55 // Rust-specific override (55%) + } +} +``` + +### Setting Payout Rates + +**Example: Set 65% payout for CS2, 55% for Rust** +```bash +curl -X PATCH https://api.turbotrades.dev/api/admin/config/instantsell \ + -H "Content-Type: application/json" \ + -H "Cookie: accessToken=YOUR_TOKEN" \ + -d '{ + "cs2": { + "payoutRate": 0.65 + }, + "rust": { + "payoutRate": 0.55 + } + }' +``` + +**Example: Disable instant sell for Rust** +```bash +curl -X PATCH https://api.turbotrades.dev/api/admin/config/instantsell \ + -H "Content-Type: application/json" \ + -H "Cookie: accessToken=YOUR_TOKEN" \ + -d '{ + "rust": { + "enabled": false + } + }' +``` + +--- + +## šŸš€ Initial Setup + +### 1. Set API Key +```bash +# In your .env file +STEAM_APIS_KEY=your_api_key_here +``` + +Get API key from: https://steamapis.com/ + +### 2. Import Initial Prices (One-Time) +```bash +# Populates MarketPrice database +node import-market-prices.js +``` + +**Expected Output:** +``` +šŸ“” Fetching CS2 market data... +āœ… Received 20,147 items from API +šŸ’¾ Importing CS2 items to database... +āœ… CS2 import complete: 20,147 inserted + +šŸ“” Fetching Rust market data... +āœ… Received 9,823 items from API +šŸ’¾ Importing Rust items to database... +āœ… Rust import complete: 9,823 inserted + +šŸŽ‰ Total: 29,970 items imported +``` + +### 3. Configure Payout Rate +Use admin panel or API to set your desired payout percentage. + +### 4. Start Server +```bash +pm2 start ecosystem.config.js --env production +``` + +Server will automatically: +- āœ… Update prices on startup +- āœ… Schedule hourly updates +- āœ… Keep prices fresh + +--- + +## šŸ“ˆ Price Update Logs + +### Successful Update: +``` +šŸ”„ Starting price update for all games... + +šŸ”„ Updating MarketPrice reference database for CS2... +šŸ“” Fetching market data from Steam API... +āœ… Received 20,147 items from API +šŸ“¦ Batch: 10,000 inserted, 10,147 updated +āœ… MarketPrice update complete for CS2: + šŸ“„ Inserted: 234 + šŸ”„ Updated: 19,913 + ā­ļø Skipped: 0 + +šŸ”„ Updating MarketPrice reference database for RUST... +āœ… Received 9,823 items from API +āœ… MarketPrice update complete for RUST: + šŸ“„ Inserted: 89 + šŸ”„ Updated: 9,734 + +āœ… All price updates complete! +``` + +### View Logs: +```bash +pm2 logs turbotrades-backend --lines 100 | grep "price update" +``` + +--- + +## šŸ› ļø Manual Price Updates + +### Update All Prices Now: +```bash +node update-prices-now.js +``` + +### Update Specific Game: +```bash +node update-prices-now.js cs2 +node update-prices-now.js rust +``` + +### Check Price Coverage: +```bash +node check-prices.js +``` + +**Output:** +``` +šŸ“Š Price Coverage Report + +šŸŽ® Counter-Strike 2: + Active Items: 1,245 + With Prices: 1,198 + Coverage: 96.2% + +šŸ”§ Rust: + Active Items: 387 + With Prices: 375 + Coverage: 96.9% +``` + +--- + +## šŸ” Troubleshooting + +### Items Showing "Price unavailable" on Sell Page + +**Cause:** MarketPrice database is empty or outdated + +**Solution:** +```bash +# Import prices +node import-market-prices.js + +# OR restart server (auto-updates on startup) +pm2 restart turbotrades-backend +``` + +### Prices Not Updating + +**Check 1:** Verify API key +```bash +# Check if set +echo $STEAM_APIS_KEY + +# Test API key +curl "https://api.steamapis.com/market/items/730?api_key=$STEAM_APIS_KEY" | head +``` + +**Check 2:** Check logs +```bash +pm2 logs turbotrades-backend --lines 200 | grep -E "price|update" +``` + +**Check 3:** Verify scheduler is running +```bash +# Should see this on startup: +ā° Starting automatic price update scheduler... +šŸ”„ Running initial price update on startup... +``` + +### Wrong Payout Rate Applied + +**Check config:** +```bash +# Via API +curl https://api.turbotrades.dev/api/config/public | jq '.config.instantSell' +``` + +**Update config:** +```bash +# Via admin panel at /admin +# Or via API (requires admin auth) +``` + +--- + +## šŸ“Š Database Queries + +### Check MarketPrice Count: +```javascript +db.marketprices.countDocuments({ game: "cs2" }) +db.marketprices.countDocuments({ game: "rust" }) +``` + +### Find Item Price: +```javascript +db.marketprices.findOne({ + marketHashName: "AK-47 | Redline (Field-Tested)", + game: "cs2" +}) +``` + +### Most Expensive Items: +```javascript +db.marketprices.find({ game: "cs2" }) + .sort({ price: -1 }) + .limit(10) +``` + +### Last Update Time: +```javascript +db.marketprices.findOne({ game: "cs2" }) + .sort({ lastUpdated: -1 }) + .limit(1) +``` + +--- + +## šŸŽÆ Best Practices + +### Payout Rates: +- **Too High** (>80%): Site loses money +- **Too Low** (<40%): Users won't sell +- **Recommended**: 55-65% depending on competition + +### Update Frequency: +- **Every Hour**: Good for active trading +- **Every 2-4 Hours**: Sufficient for most sites +- **Daily**: Only for low-volume sites + +### Monitoring: +- Set up alerts for failed price updates +- Monitor price coverage percentage +- Track user complaints about prices + +--- + +## šŸ” Security Notes + +- āœ… API key stored in `.env` (never committed) +- āœ… Payout rate changes logged with admin username +- āœ… Rate limits on price update endpoints +- āœ… Validate prices before accepting trades +- āœ… Maximum item value limits prevent abuse + +--- + +## šŸ“ Summary + +| Feature | Status | Update Frequency | +|---------|--------|------------------| +| MarketPrice DB | āœ… Automatic | Every 1 hour | +| Item Prices | āœ… Automatic | Every 1 hour | +| Payout Rate | āš™ļø Admin Config | Manual | +| Initial Import | šŸ”§ Manual | One-time | +| Sell Page | āœ… Live | Real-time | + +**Your instant sell page will now:** +- āœ… Show accurate prices from your database +- āœ… Apply your configured payout rate +- āœ… Update prices automatically every hour +- āœ… Allow per-game payout customization + +**No more manual price updates needed!** šŸŽ‰ \ No newline at end of file diff --git a/index.js b/index.js index 7201b7a..3583a1b 100644 --- a/index.js +++ b/index.js @@ -601,8 +601,24 @@ const start = async () => { .updateAllPrices() .then((result) => { console.log("āœ… Initial price update completed successfully"); - console.log(` CS2: ${result.cs2.updated || 0} items updated`); - console.log(` Rust: ${result.rust.updated || 0} items updated`); + console.log(" šŸ“Š MarketPrice Reference Database:"); + console.log( + ` CS2: ${ + result.marketPrices.cs2.updated || 0 + } prices updated, ${result.marketPrices.cs2.inserted || 0} new` + ); + console.log( + ` Rust: ${ + result.marketPrices.rust.updated || 0 + } prices updated, ${result.marketPrices.rust.inserted || 0} new` + ); + console.log(" šŸ“¦ Marketplace Items:"); + console.log( + ` CS2: ${result.itemPrices.cs2.updated || 0} items updated` + ); + console.log( + ` Rust: ${result.itemPrices.rust.updated || 0} items updated` + ); }) .catch((error) => { console.error("āŒ Initial price update failed:", error.message); diff --git a/models/SiteConfig.js b/models/SiteConfig.js index ea9d61b..89cf6e5 100644 --- a/models/SiteConfig.js +++ b/models/SiteConfig.js @@ -95,6 +95,22 @@ const SiteConfigSchema = new mongoose.Schema( priceUpdateInterval: { type: Number, default: 3600000 }, // 1 hour in ms }, + // Instant sell / buyback settings (site buying items from users) + instantSell: { + enabled: { type: Boolean, default: true }, + payoutRate: { type: Number, default: 0.6 }, // 60% of market price + minItemValue: { type: Number, default: 0.1 }, // Min $0.10 + maxItemValue: { type: Number, default: 10000 }, // Max $10,000 + cs2: { + enabled: { type: Boolean, default: true }, + payoutRate: { type: Number, default: 0.6 }, // Game-specific override + }, + rust: { + enabled: { type: Boolean, default: true }, + payoutRate: { type: Number, default: 0.6 }, // Game-specific override + }, + }, + // Features toggles features: { twoFactorAuth: { type: Boolean, default: true }, diff --git a/routes/admin-management.js b/routes/admin-management.js index 2e3e0b4..7044e3c 100644 --- a/routes/admin-management.js +++ b/routes/admin-management.js @@ -786,6 +786,98 @@ export default async function adminManagementRoutes(fastify, options) { } ); + // PATCH /admin/config/instantsell - Update instant sell settings + fastify.patch( + "/config/instantsell", + { + preHandler: [authenticate, isAdmin], + schema: { + body: { + type: "object", + properties: { + enabled: { type: "boolean" }, + payoutRate: { type: "number", minimum: 0, maximum: 1 }, + minItemValue: { type: "number", minimum: 0 }, + maxItemValue: { type: "number", minimum: 0 }, + cs2: { + type: "object", + properties: { + enabled: { type: "boolean" }, + payoutRate: { type: "number", minimum: 0, maximum: 1 }, + }, + }, + rust: { + type: "object", + properties: { + enabled: { type: "boolean" }, + payoutRate: { type: "number", minimum: 0, maximum: 1 }, + }, + }, + }, + }, + }, + }, + async (request, reply) => { + try { + const config = await SiteConfig.getConfig(); + + // Update top-level instant sell settings + ["enabled", "payoutRate", "minItemValue", "maxItemValue"].forEach( + (key) => { + if (request.body[key] !== undefined) { + config.instantSell[key] = request.body[key]; + } + } + ); + + // Update CS2 settings + if (request.body.cs2) { + Object.keys(request.body.cs2).forEach((key) => { + if (request.body.cs2[key] !== undefined) { + config.instantSell.cs2[key] = request.body.cs2[key]; + } + }); + } + + // Update Rust settings + if (request.body.rust) { + Object.keys(request.body.rust).forEach((key) => { + if (request.body.rust[key] !== undefined) { + config.instantSell.rust[key] = request.body.rust[key]; + } + }); + } + + config.lastUpdatedBy = request.user.username; + config.lastUpdatedAt = new Date(); + + await config.save(); + + console.log( + `āš™ļø Admin ${request.user.username} updated instant sell settings:`, + { + payoutRate: config.instantSell.payoutRate, + cs2PayoutRate: config.instantSell.cs2?.payoutRate, + rustPayoutRate: config.instantSell.rust?.payoutRate, + } + ); + + return reply.send({ + success: true, + message: "Instant sell settings updated", + instantSell: config.instantSell, + }); + } catch (error) { + console.error("āŒ Failed to update instant sell settings:", error); + return reply.status(500).send({ + success: false, + message: "Failed to update instant sell settings", + error: error.message, + }); + } + } + ); + // ============================================ // ANNOUNCEMENTS // ============================================ diff --git a/routes/inventory.js b/routes/inventory.js index 04fbe22..2e2d366 100644 --- a/routes/inventory.js +++ b/routes/inventory.js @@ -3,6 +3,7 @@ import { authenticate } from "../middleware/auth.js"; import Item from "../models/Item.js"; import Trade from "../models/Trade.js"; import Transaction from "../models/Transaction.js"; +import SiteConfig from "../models/SiteConfig.js"; import { config } from "../config/index.js"; import pricingService from "../services/pricing.js"; import marketPriceService from "../services/marketPrice.js"; @@ -184,6 +185,23 @@ export default async function inventoryRoutes(fastify, options) { // Enrich items with market prices (fast database lookup) console.log(`šŸ’° Adding market prices...`); + // Get site config for payout rate + const siteConfig = await SiteConfig.getConfig(); + let payoutRate = siteConfig.instantSell.payoutRate || 0.6; + + // Check for game-specific payout rate + if (game === "cs2" && siteConfig.instantSell.cs2?.payoutRate) { + payoutRate = siteConfig.instantSell.cs2.payoutRate; + } else if (game === "rust" && siteConfig.instantSell.rust?.payoutRate) { + payoutRate = siteConfig.instantSell.rust.payoutRate; + } + + console.log( + `šŸ’µ Instant sell payout rate: ${(payoutRate * 100).toFixed( + 0 + )}% of market price` + ); + // Get all item names for batch lookup const itemNames = items.map((item) => item.name); console.log(`šŸ“‹ Looking up prices for ${itemNames.length} items`); @@ -196,12 +214,19 @@ export default async function inventoryRoutes(fastify, options) { `šŸ’° Found prices for ${foundPrices}/${itemNames.length} items` ); - // Add prices to items - const enrichedItems = items.map((item) => ({ - ...item, - marketPrice: priceMap[item.name] || null, - hasPriceData: !!priceMap[item.name], - })); + // Add prices to items (applying payout rate for instant sell) + const enrichedItems = items.map((item) => { + const marketPrice = priceMap[item.name] || null; + return { + ...item, + marketPrice: marketPrice + ? parseFloat((marketPrice * payoutRate).toFixed(2)) + : null, + fullMarketPrice: marketPrice, // Keep original for reference + payoutRate: payoutRate, + hasPriceData: !!marketPrice, + }; + }); // Log items without prices const itemsWithoutPrices = enrichedItems.filter( diff --git a/services/pricing.js b/services/pricing.js index 1d218ab..8b2e2c0 100644 --- a/services/pricing.js +++ b/services/pricing.js @@ -1,5 +1,6 @@ import axios from "axios"; import Item from "../models/Item.js"; +import MarketPrice from "../models/MarketPrice.js"; /** * Pricing Service @@ -275,15 +276,177 @@ class PricingService { } /** - * Update prices for all games + * Update MarketPrice reference database for a specific game + * Fetches all items from Steam market and updates the reference database + * @param {string} game - Game identifier ('cs2' or 'rust') + * @returns {Promise} - Update statistics + */ + async updateMarketPriceDatabase(game) { + console.log( + `\nšŸ”„ Updating MarketPrice reference database for ${game.toUpperCase()}...` + ); + + try { + const appId = this.appIds[game]; + if (!appId) { + throw new Error(`Invalid game: ${game}`); + } + + if (!this.apiKey) { + throw new Error("Steam API key not configured"); + } + + // Fetch all market items from Steam API + console.log(`šŸ“” Fetching market data from Steam API...`); + const response = await axios.get( + `${this.baseUrl}/market/items/${appId}`, + { + params: { api_key: this.apiKey }, + timeout: 60000, + } + ); + + if (!response.data || !response.data.data) { + throw new Error("No data returned from Steam API"); + } + + const items = response.data.data; + const itemCount = Object.keys(items).length; + console.log(`āœ… Received ${itemCount} items from API`); + + let inserted = 0; + let updated = 0; + let skipped = 0; + let errors = 0; + + // Process items in batches + const bulkOps = []; + + for (const item of Object.values(items)) { + try { + // Get the best available price + const price = + item.prices?.safe || + item.prices?.median || + item.prices?.mean || + item.prices?.avg || + item.prices?.latest; + + if (!price || price <= 0) { + skipped++; + continue; + } + + const marketHashName = item.market_hash_name || item.market_name; + const marketName = item.market_name || item.market_hash_name; + + if (!marketHashName || !marketName) { + skipped++; + continue; + } + + // Determine which price type was used + let priceType = "safe"; + if (item.prices?.safe) priceType = "safe"; + else if (item.prices?.median) priceType = "median"; + else if (item.prices?.mean) priceType = "mean"; + else if (item.prices?.avg) priceType = "avg"; + else if (item.prices?.latest) priceType = "latest"; + + bulkOps.push({ + updateOne: { + filter: { marketHashName: marketHashName }, + update: { + $set: { + name: marketName, + game: game, + appId: appId, + marketHashName: marketHashName, + price: price, + priceType: priceType, + image: item.image || null, + borderColor: item.border_color || null, + nameId: item.nameID || null, + lastUpdated: new Date(), + }, + }, + upsert: true, + }, + }); + + // Execute in batches of 1000 + if (bulkOps.length >= 1000) { + const result = await MarketPrice.bulkWrite(bulkOps); + inserted += result.upsertedCount; + updated += result.modifiedCount; + console.log( + ` šŸ“¦ Batch: ${inserted} inserted, ${updated} updated` + ); + bulkOps.length = 0; + } + } catch (err) { + errors++; + } + } + + // Execute remaining items + if (bulkOps.length > 0) { + const result = await MarketPrice.bulkWrite(bulkOps); + inserted += result.upsertedCount; + updated += result.modifiedCount; + } + + console.log(`āœ… MarketPrice update complete for ${game.toUpperCase()}:`); + console.log(` šŸ“„ Inserted: ${inserted}`); + console.log(` šŸ”„ Updated: ${updated}`); + console.log(` ā­ļø Skipped: ${skipped}`); + if (errors > 0) { + console.log(` āŒ Errors: ${errors}`); + } + + return { + success: true, + game, + total: itemCount, + inserted, + updated, + skipped, + errors, + }; + } catch (error) { + console.error( + `āŒ Error updating MarketPrice for ${game}:`, + error.message + ); + return { + success: false, + game, + error: error.message, + total: 0, + inserted: 0, + updated: 0, + skipped: 0, + errors: 1, + }; + } + } + + /** + * Update prices for all games (both Item and MarketPrice databases) * @returns {Promise} - Combined update statistics */ async updateAllPrices() { console.log("šŸ”„ Starting price update for all games..."); const results = { - cs2: await this.updateDatabasePrices("cs2"), - rust: await this.updateDatabasePrices("rust"), + marketPrices: { + cs2: await this.updateMarketPriceDatabase("cs2"), + rust: await this.updateMarketPriceDatabase("rust"), + }, + itemPrices: { + cs2: await this.updateDatabasePrices("cs2"), + rust: await this.updateDatabasePrices("rust"), + }, timestamp: new Date(), };