/** * GoldRate Engine — Express Server * * Serves Gold 24K and Silver 999 prices from two sources: * - SLN Bullion Chennai (local Chennai rates) * - IBJA India via goldratetodaylive.in (national rates) * * Endpoints: * GET /api/rates → Prices based on API key's source (SLN or IBJA) * GET /api/sln-rates → SLN Bullion Chennai rates (for frontend) * GET /api/ibja-rates → IBJA India rates (for frontend) * GET /api/history → SLN historical prices * GET /api/ibja-history → IBJA historical prices * GET /health → Server health check * * Authentication: * Header: x-api-key: YOUR_API_KEY * OR Query: ?apikey=YOUR_API_KEY */ require('dotenv').config(); const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const { getGoldRate, getIBJARate } = require('./priceFetcher'); const { authMiddleware } = require('./auth'); const { connect: connectDB, getIsConnected, getClient } = require('./db'); const { initTelegramBot } = require('./telegramBot'); // Middleware to secure public endpoints (allows same-origin referers, custom headers, or valid API keys) const dashboardAuthMiddleware = (req, res, next) => { const apiKey = req.headers['x-api-key'] || req.query.apikey; if (apiKey) { return authMiddleware(req, res, next); } // Allow requests from RapidAPI proxy (strictly authenticated using the proxy secret key) if (req.headers['x-rapidapi-proxy-secret'] && req.headers['x-rapidapi-proxy-secret'] === process.env.RAPIDAPI_PROXY_SECRET) { req.priceSource = 'SLN'; // Default to SLN return next(); } // Allow requests containing our secure frontend dashboard verification header or query param if (req.headers['x-app-request'] === 'goldscrape-dashboard' || req.query.dashboard === 'true') { req.priceSource = 'SLN'; // Default to SLN return next(); } const referer = req.headers.referer || req.headers.referrer; const host = req.headers.host; // Allow same-origin, Hugging Face Space iframes, Render domains, and localhost const isAllowedOrigin = referer && ( (host && referer.includes(host)) || referer.includes('huggingface.co') || referer.includes('onrender.com') || referer.includes('localhost') ); if (isAllowedOrigin) { req.priceSource = 'SLN'; // Default to SLN return next(); } return res.status(401).json({ success: false, error: 'API key required for external requests.' }); }; // Connect to MongoDB (if URI present) connectDB(); const app = express(); const PORT = process.env.PORT || 3000; // Trust proxy header to allow correct IP extraction behind Hugging Face/Render proxies app.set('trust proxy', 1); // ─── Security ──────────────────────────────────────── app.use(helmet({ contentSecurityPolicy: false, frameguard: false })); app.use(cors()); app.use(express.json()); // ─── Serve Frontend ─────────────────────────────────── app.use(express.static('public')); // Explicitly serve index.html for the root route app.get('/', (req, res) => { const path = require('path'); res.sendFile(path.join(__dirname, '../public/index.html')); }); // ─── Rate Limiting ─────────────────────────────────── const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: parseInt(process.env.RATE_LIMIT_MAX) || 500, skip: (req) => { return req.headers['x-app-request'] === 'goldscrape-dashboard' || req.query.dashboard === 'true' || (req.headers['x-rapidapi-proxy-secret'] && req.headers['x-rapidapi-proxy-secret'] === process.env.RAPIDAPI_PROXY_SECRET); }, message: { success: false, error: 'Too many requests. Please try again later.' } }); app.use('/api', limiter); // ─── Track server start time ───────────────────────── const serverStartTime = new Date(); let lastSlnSaveTime = 0; let lastIbjaSaveTime = 0; // Warm rate caches in the background every 30 seconds setInterval(async () => { try { await getGoldRate(); await getIBJARate(); } catch (err) { console.warn('⚠️ Background cache warming failed:', err.message); } }, 30000); // ─── Health Check (No API key needed) ──────────────── app.get('/health', (req, res) => { const uptime = Math.floor((Date.now() - serverStartTime.getTime()) / 1000); res.json({ success: true, status: 'running', uptime: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m ${uptime % 60}s`, serverTime: new Date().toLocaleString('en-IN', { timeZone: 'Asia/Kolkata' }), sources: ['SLN Bullion Chennai', 'IBJA India'], version: '2.0.0' }); }); // ─── Helper: Save price to DB ──────────────────────── const DB_SAVE_INTERVAL = 30 * 60 * 1000; // 30 minutes async function saveSLNToDb(data) { if (!getIsConnected() || (Date.now() - lastSlnSaveTime) < DB_SAVE_INTERVAL) return; try { const supabase = getClient(); const today = data.rateDate || new Date().toISOString().split('T')[0]; // Explicitly round to 2 decimals const goldRounded = data.gold24k ? Math.round(data.gold24k * 100) / 100 : null; const silverPerGram = data.silver999 ? Math.round((data.silver999 / 1000) * 100) / 100 : null; await supabase .from('price_records') .upsert({ date: today, gold24k: goldRounded, silver999: silverPerGram, source: 'SLN' }); lastSlnSaveTime = Date.now(); console.log(`💾 SLN DB saved: ₹${goldRounded}/g`); } catch (e) { console.warn('⚠️ SLN DB save failed:', e.message); } } async function saveIBJAToDb(data) { if (!getIsConnected() || (Date.now() - lastIbjaSaveTime) < DB_SAVE_INTERVAL) return; try { const supabase = getClient(); const today = data.rateDate || new Date().toISOString().split('T')[0]; // Explicitly round to 2 decimals const goldRounded = data.gold24k ? Math.round(data.gold24k * 100) / 100 : null; const rawSilver = data.silver999; const silverPerGram = rawSilver ? (rawSilver > 500 ? Math.round((rawSilver / 1000) * 100) / 100 : Math.round(rawSilver * 100) / 100) : null; await supabase .from('ibja_price_records') .upsert({ date: today, gold24k: goldRounded, silver999: silverPerGram, source: 'IBJA' }); lastIbjaSaveTime = Date.now(); console.log(`💾 IBJA DB saved: ₹${goldRounded}/g`); } catch (e) { console.warn('⚠️ IBJA DB save failed:', e.message); } } // ─── Main API: Rates based on API key source ───────── app.get('/api/rates', authMiddleware, async (req, res) => { try { const source = req.priceSource || 'SLN'; // If master key or explicitly configured for BOTH sources if (req.isMasterKey || source === 'BOTH') { const slnData = await getGoldRate(); const ibjaData = await getIBJARate(); saveSLNToDb(slnData); saveIBJAToDb(ibjaData); const slnSilver = slnData.silver999 ? Math.round((slnData.silver999 / 1000) * 100) / 100 : null; const ibjaSilver = ibjaData.silver999 ? (ibjaData.silver999 > 500 ? Math.round((ibjaData.silver999 / 1000) * 100) / 100 : ibjaData.silver999) : null; res.set('Cache-Control', 'public, max-age=15'); return res.json({ success: true, rates: { sln: { gold24k_1gram: slnData.gold24k, silver999_1gram: slnSilver, currency: 'INR', source: slnData.source, session: slnData.session, date: slnData.rateDate, updatedAt: slnData.fetchedAt }, ibja: { gold24k_1gram: ibjaData.gold24k, silver999_1gram: ibjaSilver, currency: 'INR', source: ibjaData.source, session: ibjaData.session, date: ibjaData.rateDate, updatedAt: ibjaData.fetchedAt } } }); } // Default: Single source rates based on key configuration let data; if (source === 'IBJA') { data = await getIBJARate(); saveIBJAToDb(data); } else { data = await getGoldRate(); saveSLNToDb(data); } const silverPerGram = data.silver999 ? (data.silver999 > 500 ? Math.round((data.silver999 / 1000) * 100) / 100 : data.silver999) : null; res.set('Cache-Control', 'public, max-age=15'); res.json({ success: true, gold24k_1gram: data.gold24k, silver999_1gram: silverPerGram, currency: 'INR', source: data.source, session: data.session, date: data.rateDate, updatedAt: data.fetchedAt }); } catch (err) { console.error('❌ API Error:', err.message); res.status(503).json({ success: false, error: 'Unable to fetch prices right now. Please try again.', details: err.message }); } }); // ─── SLN Rates (for frontend) ──────────────────────── app.get('/api/sln-rates', dashboardAuthMiddleware, async (req, res) => { try { const data = await getGoldRate(); saveSLNToDb(data); const silverPerGram = data.silver999 ? Math.round((data.silver999 / 1000) * 100) / 100 : null; res.set('Cache-Control', 'public, max-age=15'); res.json({ success: true, gold24k_1gram: data.gold24k, silver999_1gram: silverPerGram, currency: 'INR', source: data.source, session: data.session, date: data.rateDate, updatedAt: data.fetchedAt }); } catch (err) { res.status(503).json({ success: false, error: err.message }); } }); // ─── IBJA Rates (for frontend) ─────────────────────── app.get('/api/ibja-rates', dashboardAuthMiddleware, async (req, res) => { try { const data = await getIBJARate(); saveIBJAToDb(data); const silverPerGram = data.silver999 ? (data.silver999 > 500 ? Math.round((data.silver999 / 1000) * 100) / 100 : data.silver999) : null; res.set('Cache-Control', 'public, max-age=15'); res.json({ success: true, gold24k_1gram: data.gold24k, silver999_1gram: silverPerGram, currency: 'INR', source: data.source, session: data.session, date: data.rateDate, updatedAt: data.fetchedAt }); } catch (err) { res.status(503).json({ success: false, error: 'IBJA source unavailable', details: err.message }); } }); // Memory cache for historical records const slnHistoryCache = {}; const ibjaHistoryCache = {}; const HISTORY_CACHE_DURATION = 60 * 60 * 1000; // 1 hour // ─── Historical Prices (SLN) ───────────────────────── const RANGE_DAYS = { '1m': 30, '3m': 90, '6m': 180, '1y': 365, '3y': 1095 }; app.get('/api/history', dashboardAuthMiddleware, async (req, res) => { if (!getIsConnected()) { return res.status(503).json({ success: false, error: 'History not available — database not configured' }); } try { const range = (req.query.range || '3m').toLowerCase(); const now = Date.now(); // Check memory cache if (slnHistoryCache[range] && (now - slnHistoryCache[range].time) < HISTORY_CACHE_DURATION) { return res.json(slnHistoryCache[range].data); } const days = RANGE_DAYS[range] || 90; const startDate = new Date(); startDate.setDate(startDate.getDate() - days); const startStr = startDate.toISOString().split('T')[0]; const supabase = getClient(); const { data: records } = await supabase .from('price_records') .select('date, gold24k, silver999') .gte('date', startStr) .order('date', { ascending: true }); // Calculate performance let performance = null; if (records.length >= 2) { const first = records[0].gold24k; const last = records[records.length - 1].gold24k; const change = last - first; const percent = ((change / first) * 100).toFixed(2); performance = { startPrice: first, endPrice: last, change: Math.round(change * 100) / 100, percentChange: parseFloat(percent), direction: change >= 0 ? 'UP' : 'DOWN' }; } const responseJson = { success: true, source: 'SLN Bullion Chennai', range: range, totalDays: records.length, performance: performance, data: records }; // Save to memory cache slnHistoryCache[range] = { time: now, data: responseJson }; res.json(responseJson); } catch (err) { console.error('❌ History Error:', err.message); res.status(500).json({ success: false, error: 'Failed to fetch history' }); } }); // ─── Historical Prices (IBJA) ──────────────────────── app.get('/api/ibja-history', dashboardAuthMiddleware, async (req, res) => { if (!getIsConnected()) { return res.status(503).json({ success: false, error: 'History not available — database not configured' }); } try { const range = (req.query.range || '3m').toLowerCase(); const now = Date.now(); // Check memory cache if (ibjaHistoryCache[range] && (now - ibjaHistoryCache[range].time) < HISTORY_CACHE_DURATION) { return res.json(ibjaHistoryCache[range].data); } const days = RANGE_DAYS[range] || 90; const startDate = new Date(); startDate.setDate(startDate.getDate() - days); const startStr = startDate.toISOString().split('T')[0]; const supabase = getClient(); const { data: records } = await supabase .from('ibja_price_records') .select('date, gold24k, silver999') .gte('date', startStr) .order('date', { ascending: true }); let performance = null; if (records && records.length >= 2) { const first = records[0].gold24k; const last = records[records.length - 1].gold24k; const change = last - first; const percent = ((change / first) * 100).toFixed(2); performance = { startPrice: first, endPrice: last, change: Math.round(change * 100) / 100, percentChange: parseFloat(percent), direction: change >= 0 ? 'UP' : 'DOWN' }; } const responseJson = { success: true, source: 'IBJA India', range: range, totalDays: records ? records.length : 0, performance: performance, data: records || [] }; // Save to memory cache ibjaHistoryCache[range] = { time: now, data: responseJson }; res.json(responseJson); } catch (err) { console.error('❌ IBJA History Error:', err.message); res.status(500).json({ success: false, error: 'Failed to fetch IBJA history' }); } }); // ─── SEO Landing Pages Engine ──────────────────────── const serveSEOPage = (req, res, seoData) => { try { const htmlPath = path.join(__dirname, '../public/index.html'); if (!fs.existsSync(htmlPath)) { return res.status(404).send('Dashboard template not found'); } let html = fs.readFileSync(htmlPath, 'utf8'); // Replace Meta Title if (seoData.title) { html = html.replace(/[^<]*<\/title>/, `<title>${seoData.title}`); html = html.replace(/[\s\S]*?<\/h1>/, `

${seoData.h1}

`); } res.send(html); } catch (err) { console.error('❌ SEO Render Error:', err.message); res.status(500).send('Error rendering page'); } }; const seoRoutes = [ { path: '/gold-rate-api', title: 'Gold Rate API India | Real-time Gold Price Feed', description: 'Free and premium Gold Rate API for India. Get live 22K and 24K gold prices and developer documentation.', h1: 'Gold Rate API India' }, { path: '/live-gold-api', title: 'Live Gold Price API | Real-Time Bullion Prices', description: 'High-speed live gold price API for developers. Integrate spot gold rates with under 10ms response times.', h1: 'Live Gold Price API' }, { path: '/chennai-gold-rate-api', title: 'Chennai Gold Rate API | Live SLN Bullion Price Feed', description: 'Get real-time retail and wholesale gold spot rates in Chennai directly from SLN Bullion Chennai.', h1: 'Chennai Gold Rate API' }, { path: '/india-gold-api', title: 'India Gold API | Official IBJA Bullion Price Feed', description: 'Access official gold and silver price feeds for India based on IBJA national bullion standards.', h1: 'India Gold API & Silver Price API' }, { path: '/gold-price-api', title: 'Gold Price API | Real-Time Gold Price JSON Feed', description: 'Reliable B2B Gold Price API for jewellery stores, finance apps, and ERP systems.', h1: 'Gold Price API for Developers' }, { path: '/silver-price-api', title: 'Silver Price API | Live Silver Spot Rates', description: 'Get live Silver 999 price API feeds for India. Under 10ms cached proxy responses.', h1: 'Silver Price API' }, { path: '/historical-gold-api', title: 'Historical Gold Price API India | Historical Bullion Data', description: 'Retrieve historical gold and silver price records for Chennai and India. Includes 1-month to 3-year history.', h1: 'Historical Gold Price API India' }, { path: '/rest-api', title: 'Gold Rate JSON API | Gold Price REST API', description: 'Easy integration using our RESTful JSON API. Complete with code examples in Node.js, Python, and cURL.', h1: 'Gold Rate JSON API' }, { path: '/json-api', title: 'Gold Rate JSON API | Gold Price REST API', description: 'Easy integration using our RESTful JSON API. Complete with code examples in Node.js, Python, and cURL.', h1: 'Gold Rate JSON API' }, { path: '/docs', title: 'GoldScrape API Documentation | Developer Guides', description: 'Developer guides, JSON response templates, and implementation code snippets in Node.js, Python, and PHP.', h1: 'GoldScrape API Documentation' }, { path: '/pricing', title: 'GoldScrape API Pricing | Gold API Subscription Plans', description: 'Choose between Monthly License (₹599/mo) and Yearly License (₹3,999/yr) plans. Start a 7-day free trial.', h1: 'GoldScrape API Pricing' } ]; seoRoutes.forEach(route => { app.get(route.path, (req, res) => { serveSEOPage(req, res, route); }); }); // Dynamic City Specific Landing Pages app.get('/city/:cityName', (req, res) => { const citySlug = req.params.cityName; let cityName = citySlug.split('-')[0]; cityName = cityName.charAt(0).toUpperCase() + cityName.slice(1); const seoData = { title: `${cityName} Gold Rate API | Live Gold Price in ${cityName}`, description: `Get real-time gold and silver spot prices in ${cityName}, Tamil Nadu. Live updates, fallbacks, and developer guides.`, h1: `${cityName} Gold Rate API` }; serveSEOPage(req, res, seoData); }); // ─── Google Ads Conversion Page ────────────────────── app.get('/thank-you', (req, res) => { const plan = req.query.plan || 'general'; let waText = 'Hi, I want to inquire about GoldScrape API.'; if (plan === 'monthly') { waText = 'Hi, I want to subscribe to the Monthly Plan (₹599/mo) for GoldScrape API.'; } else if (plan === 'yearly') { waText = 'Hi, I want to subscribe to the Yearly Plan (₹3,999/yr) for GoldScrape API.'; } else if (plan === 'enterprise') { waText = 'Hi, I want to inquire about the Enterprise Plan for GoldScrape API.'; } const waUrl = `https://wa.me/919360345770?text=${encodeURIComponent(waText)}`; res.send(` Thank You - GoldScrape API

Thank You!

Your request has been received. Redirecting you to WhatsApp to complete your activation...

`); }); // ─── 404 Handler ───────────────────────────────────── app.use((req, res) => { res.status(404).json({ success: false, error: 'Endpoint not found', availableEndpoints: { rates: 'GET /api/rates?apikey=YOUR_KEY (source based on key)', slnRates: 'GET /api/sln-rates?apikey=YOUR_KEY', ibjaRates: 'GET /api/ibja-rates?apikey=YOUR_KEY', history: 'GET /api/history?range=1m|3m|6m|1y|3y&apikey=YOUR_KEY', ibjaHistory: 'GET /api/ibja-history?range=1m|3m|6m|1y|3y&apikey=YOUR_KEY', health: 'GET /health' } }); }); // ─── Start Server ──────────────────────────────────── app.listen(PORT, () => { console.log(''); console.log('╔══════════════════════════════════════════════════╗'); console.log('║ 🪙 GoldRate Engine v2.0.0 🪙 ║'); console.log('║ SLN Bullion Chennai + IBJA India Live API ║'); console.log('╠══════════════════════════════════════════════════╣'); console.log(`║ Server: http://localhost:${PORT} ║`); console.log(`║ SLN Rates: /api/sln-rates ║`); console.log(`║ IBJA Rates: /api/ibja-rates ║`); console.log(`║ SLN History: /api/history ║`); console.log(`║ IBJA History: /api/ibja-history ║`); console.log(`║ Health: /health ║`); console.log('╠══════════════════════════════════════════════════╣'); console.log(`║ Master Key: ${process.env.API_KEY ? '✅ Loaded' : '❌ Missing!'} ║`); console.log(`║ Sources: SLN Bullion + IBJA ║`); console.log(`║ Key Expiry: 30 days (Supabase api_keys table) ║`); console.log(`║ Telegram: ${process.env.TELEGRAM_BOT_TOKEN ? '✅ Bot Active' : '❌ Not configured'} ║`); console.log('╚══════════════════════════════════════════════════╝'); console.log(''); console.log('📱 API Key Management:'); console.log(' 🤖 Telegram: /create_sln or /create_ibja'); console.log(' 💻 CLI: node scripts/manage-keys.js create --name "Shop" --source SLN'); console.log(''); // Start Telegram Bot initTelegramBot(); // Start background price saver to guarantee historical records without client traffic startBackgroundSaver(); }); // ─── Background Price Saver (no traffic required) ───── function startBackgroundSaver() { console.log('⏰ Starting background price saver (interval: 1 hour)...'); const saveRates = async () => { try { const slnData = await getGoldRate(); const slnToday = slnData.rateDate || new Date().toISOString().split('T')[0]; const slnGoldRounded = slnData.gold24k ? Math.round(slnData.gold24k * 100) / 100 : null; const slnSilverPerGram = slnData.silver999 ? Math.round((slnData.silver999 / 1000) * 100) / 100 : null; const supabase = getClient(); if (getIsConnected()) { await supabase .from('price_records') .upsert({ date: slnToday, gold24k: slnGoldRounded, silver999: slnSilverPerGram, source: 'SLN' }); console.log(`⏰ Background Auto-Save: SLN Bullion saved (₹${slnGoldRounded}/g)`); try { const ibjaData = await getIBJARate(); const ibjaToday = ibjaData.rateDate || new Date().toISOString().split('T')[0]; const ibjaGoldRounded = ibjaData.gold24k ? Math.round(ibjaData.gold24k * 100) / 100 : null; const rawSilver = ibjaData.silver999; const ibjaSilverPerGram = rawSilver ? (rawSilver > 500 ? Math.round((rawSilver / 1000) * 100) / 100 : Math.round(rawSilver * 100) / 100) : null; await supabase .from('ibja_price_records') .upsert({ date: ibjaToday, gold24k: ibjaGoldRounded, silver999: ibjaSilverPerGram, source: 'IBJA' }); console.log(`⏰ Background Auto-Save: IBJA India saved (₹${ibjaGoldRounded}/g)`); } catch (e) { console.warn('⚠️ Background IBJA save failed:', e.message); } } } catch (err) { console.error('⚠️ Background price saver failed:', err.message); } }; // Run initial save after 10 seconds setTimeout(saveRates, 10000); // Repeat every 1 hour const ONE_HOUR = 60 * 60 * 1000; setInterval(saveRates, ONE_HOUR); } module.exports = app;