diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..fcc0e43
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,51 @@
+# Dockerfile for Next.js 15 + Puppeteer/Chromium + Prisma + Tailwind + OpenAI
+FROM node:20-slim
+
+# Install system dependencies for Chromium
+RUN apt-get update && apt-get install -y \
+ wget \
+ ca-certificates \
+ fonts-liberation \
+ libappindicator3-1 \
+ libasound2 \
+ libatk-bridge2.0-0 \
+ libatk1.0-0 \
+ libcups2 \
+ libdbus-1-3 \
+ libdrm2 \
+ libgbm1 \
+ libnspr4 \
+ libnss3 \
+ libx11-xcb1 \
+ libxcomposite1 \
+ libxdamage1 \
+ libxrandr2 \
+ xdg-utils \
+ --no-install-recommends && \
+ rm -rf /var/lib/apt/lists/*
+
+# Install Chromium (Debian 12+ uses 'chromium' instead of 'chromium-browser')
+RUN apt-get update && apt-get install -y \
+ chromium \
+ --no-install-recommends && \
+ rm -rf /var/lib/apt/lists/*
+
+# Set working directory
+WORKDIR /app
+
+# Copy package files and install dependencies
+COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* .npmrc* ./
+RUN npm install
+
+# Copy the rest of the app
+COPY . .
+
+# Expose port
+EXPOSE 3000
+
+# Set environment variables for Puppeteer
+ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
+ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
+
+# Start the app
+CMD ["npm", "run", "dev"]
diff --git a/app/api/analyze/route.ts b/app/api/analyze/route.ts
new file mode 100644
index 0000000..38d9009
--- /dev/null
+++ b/app/api/analyze/route.ts
@@ -0,0 +1,53 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { aiAnalysisService } from '../../../lib/ai-analysis'
+import { tradingViewCapture } from '../../../lib/tradingview'
+import { settingsManager } from '../../../lib/settings'
+import path from 'path'
+
+export async function POST(req: NextRequest) {
+ try {
+ const { symbol, layouts, timeframe } = await req.json()
+
+ // Load current settings
+ const settings = await settingsManager.loadSettings()
+
+ // Use provided values or fall back to saved settings
+ const finalSymbol = symbol || settings.symbol
+ const finalTimeframe = timeframe || settings.timeframe
+ const finalLayouts = layouts || settings.layouts
+
+ if (!finalSymbol) {
+ return NextResponse.json({ error: 'Missing symbol' }, { status: 400 })
+ }
+
+ const baseFilename = `${finalSymbol}_${finalTimeframe}_${Date.now()}`
+ const screenshots = await tradingViewCapture.capture(finalSymbol, `${baseFilename}.png`, finalLayouts, finalTimeframe)
+
+ let result
+ if (screenshots.length === 1) {
+ // Single screenshot analysis
+ const filename = path.basename(screenshots[0])
+ result = await aiAnalysisService.analyzeScreenshot(filename)
+ } else {
+ // Multiple screenshots analysis
+ const filenames = screenshots.map(screenshot => path.basename(screenshot))
+ result = await aiAnalysisService.analyzeMultipleScreenshots(filenames)
+ }
+
+ if (!result) {
+ return NextResponse.json({ error: 'Analysis failed' }, { status: 500 })
+ }
+
+ return NextResponse.json({
+ ...result,
+ settings: {
+ symbol: finalSymbol,
+ timeframe: finalTimeframe,
+ layouts: finalLayouts
+ },
+ screenshots: screenshots.map(s => path.basename(s))
+ })
+ } catch (e: any) {
+ return NextResponse.json({ error: e.message }, { status: 500 })
+ }
+}
diff --git a/app/api/auto-trading/route.ts b/app/api/auto-trading/route.ts
new file mode 100644
index 0000000..1826bb3
--- /dev/null
+++ b/app/api/auto-trading/route.ts
@@ -0,0 +1,33 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { getAutoTradingService } from '../../../lib/auto-trading'
+
+const autoTradingService = getAutoTradingService()
+
+export async function POST(req: NextRequest) {
+ try {
+ const { action, config } = await req.json()
+ if (action === 'start') {
+ autoTradingService.start()
+ return NextResponse.json({ status: 'started' })
+ }
+ if (action === 'stop') {
+ autoTradingService.stop()
+ return NextResponse.json({ status: 'stopped' })
+ }
+ if (action === 'config' && config) {
+ autoTradingService.setConfig(config)
+ return NextResponse.json({ status: 'config updated', config: autoTradingService })
+ }
+ return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
+ } catch (e: any) {
+ return NextResponse.json({ error: e.message }, { status: 500 })
+ }
+}
+
+export async function GET() {
+ // Return current config/status
+ return NextResponse.json({
+ config: autoTradingService,
+ running: !!autoTradingService['intervalId']
+ })
+}
diff --git a/app/api/screenshot/route.ts b/app/api/screenshot/route.ts
new file mode 100644
index 0000000..a91facf
--- /dev/null
+++ b/app/api/screenshot/route.ts
@@ -0,0 +1,15 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { tradingViewCapture } from '../../../lib/tradingview'
+
+export async function POST(req: NextRequest) {
+ try {
+ const { symbol, filename } = await req.json()
+ if (!symbol || !filename) {
+ return NextResponse.json({ error: 'Missing symbol or filename' }, { status: 400 })
+ }
+ const filePath = await tradingViewCapture.capture(symbol, filename)
+ return NextResponse.json({ filePath })
+ } catch (e: any) {
+ return NextResponse.json({ error: e.message }, { status: 500 })
+ }
+}
diff --git a/app/api/trading-history/route.ts b/app/api/trading-history/route.ts
new file mode 100644
index 0000000..3c37871
--- /dev/null
+++ b/app/api/trading-history/route.ts
@@ -0,0 +1,14 @@
+import prisma from '../../../lib/prisma'
+import { NextResponse } from 'next/server'
+
+export async function GET() {
+ try {
+ const trades = await prisma.trade.findMany({
+ orderBy: { executedAt: 'desc' },
+ take: 50
+ })
+ return NextResponse.json(trades)
+ } catch (e: any) {
+ return NextResponse.json({ error: e.message }, { status: 500 })
+ }
+}
diff --git a/app/api/trading/route.ts b/app/api/trading/route.ts
new file mode 100644
index 0000000..43ae72c
--- /dev/null
+++ b/app/api/trading/route.ts
@@ -0,0 +1,21 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { driftTradingService } from '../../../lib/drift-trading'
+
+export async function POST(req: NextRequest) {
+ try {
+ const params = await req.json()
+ const result = await driftTradingService.executeTrade(params)
+ return NextResponse.json(result)
+ } catch (e: any) {
+ return NextResponse.json({ error: e.message }, { status: 500 })
+ }
+}
+
+export async function GET() {
+ try {
+ const positions = await driftTradingService.getPositions()
+ return NextResponse.json({ positions })
+ } catch (e: any) {
+ return NextResponse.json({ error: e.message }, { status: 500 })
+ }
+}
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..ef59836
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,7 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ font-family: 'Inter', system-ui, sans-serif;
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..375ce91
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,19 @@
+import './globals.css'
+import type { Metadata } from 'next'
+
+export const metadata: Metadata = {
+ title: 'Trading Bot Dashboard',
+ description: 'AI-powered trading bot dashboard with auto-trading, analysis, and developer tools.'
+}
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..0c5d743
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,11 @@
+import AIAnalysisPanel from '../components/AIAnalysisPanel'
+import Dashboard from '../components/Dashboard'
+
+export default function HomePage() {
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/app/screenshots/[filename]/route.ts b/app/screenshots/[filename]/route.ts
new file mode 100644
index 0000000..1a00594
--- /dev/null
+++ b/app/screenshots/[filename]/route.ts
@@ -0,0 +1,22 @@
+import { NextRequest, NextResponse } from 'next/server'
+import fs from 'fs/promises'
+import path from 'path'
+
+export async function GET(
+ request: NextRequest,
+ context: any
+) {
+ try {
+ const screenshotsDir = path.join(process.cwd(), 'screenshots')
+ const filePath = path.join(screenshotsDir, context.params.filename)
+ const file = await fs.readFile(filePath)
+ return new NextResponse(file, {
+ headers: {
+ 'Content-Type': 'image/png',
+ 'Content-Disposition': `inline; filename="${context.params.filename}"`
+ }
+ })
+ } catch (e: any) {
+ return NextResponse.json({ error: e.message }, { status: 404 })
+ }
+}
diff --git a/components/AIAnalysisPanel.tsx b/components/AIAnalysisPanel.tsx
new file mode 100644
index 0000000..f287e11
--- /dev/null
+++ b/components/AIAnalysisPanel.tsx
@@ -0,0 +1,106 @@
+"use client"
+import React, { useState } from 'react'
+
+const layouts = (process.env.NEXT_PUBLIC_TRADINGVIEW_LAYOUTS || 'ai,Diy module').split(',').map(l => l.trim())
+const timeframes = [
+ { label: '1m', value: '1' },
+ { label: '5m', value: '5' },
+ { label: '15m', value: '15' },
+ { label: '1h', value: '60' },
+ { label: '4h', value: '240' },
+ { label: '1d', value: 'D' },
+ { label: '1w', value: 'W' },
+ { label: '1M', value: 'M' },
+]
+
+export default function AIAnalysisPanel() {
+ const [symbol, setSymbol] = useState('BTCUSD')
+ const [layout, setLayout] = useState(layouts[0])
+ const [timeframe, setTimeframe] = useState('60')
+ const [loading, setLoading] = useState(false)
+ const [result, setResult] = useState(null)
+ const [error, setError] = useState(null)
+
+ async function handleAnalyze() {
+ setLoading(true)
+ setError(null)
+ setResult(null)
+ try {
+ const res = await fetch('/api/analyze', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ symbol, layout, timeframe })
+ })
+ const data = await res.json()
+ if (!res.ok) throw new Error(data.error || 'Unknown error')
+ setResult(data)
+ } catch (e: any) {
+ setError(e.message)
+ }
+ setLoading(false)
+ }
+
+ return (
+
+
AI Chart Analysis
+
+ setSymbol(e.target.value)}
+ placeholder="Symbol (e.g. BTCUSD)"
+ />
+
+
+
+
+ {error && (
+
+ {error.includes('frame was detached') ? (
+ <>
+ TradingView chart could not be loaded. Please check your symbol and layout, or try again.
+ (Technical: {error})
+ >
+ ) : error.includes('layout not found') ? (
+ <>
+ TradingView layout not found. Please select a valid layout.
+ (Technical: {error})
+ >
+ ) : (
+ error
+ )}
+
+ )}
+ {loading && (
+
+ )}
+ {result && (
+
+
Summary: {result.summary}
+
Sentiment: {result.marketSentiment}
+
Recommendation: {result.recommendation} ({result.confidence}%)
+
Support: {result.keyLevels?.support?.join(', ')}
+
Resistance: {result.keyLevels?.resistance?.join(', ')}
+
Reasoning: {result.reasoning}
+
+ )}
+
+ )
+}
diff --git a/components/AutoTradingPanel.tsx b/components/AutoTradingPanel.tsx
new file mode 100644
index 0000000..f39dd0e
--- /dev/null
+++ b/components/AutoTradingPanel.tsx
@@ -0,0 +1,35 @@
+"use client"
+import React, { useState } from 'react'
+
+export default function AutoTradingPanel() {
+ const [status, setStatus] = useState<'idle'|'running'|'stopped'>('idle')
+ const [message, setMessage] = useState('')
+
+ async function handleAction(action: 'start'|'stop') {
+ setMessage('')
+ setStatus('idle')
+ const res = await fetch('/api/auto-trading', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ action })
+ })
+ if (res.ok) {
+ setStatus(action === 'start' ? 'running' : 'stopped')
+ setMessage(`Auto-trading ${action}ed`)
+ } else {
+ setMessage('Error: ' + (await res.text()))
+ }
+ }
+
+ return (
+
+
Auto-Trading Control
+
+
+
+
+
Status: {status}
+ {message &&
{message}
}
+
+ )
+}
diff --git a/components/Dashboard-minimal.tsx b/components/Dashboard-minimal.tsx
new file mode 100644
index 0000000..2212a09
--- /dev/null
+++ b/components/Dashboard-minimal.tsx
@@ -0,0 +1,11 @@
+"use client"
+import React from 'react'
+
+export default function DashboardMinimal() {
+ return (
+
+
Trading Bot Dashboard
+
Welcome! Please select a feature from the menu.
+
+ )
+}
diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx
new file mode 100644
index 0000000..f66ce69
--- /dev/null
+++ b/components/Dashboard.tsx
@@ -0,0 +1,68 @@
+"use client"
+import React, { useEffect, useState } from 'react'
+import AutoTradingPanel from './AutoTradingPanel'
+import TradingHistory from './TradingHistory'
+import DeveloperSettings from './DeveloperSettings'
+
+export default function Dashboard() {
+ const [positions, setPositions] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ async function fetchPositions() {
+ try {
+ const res = await fetch('/api/trading')
+ if (res.ok) {
+ const data = await res.json()
+ setPositions(data.positions || [])
+ } else {
+ setError('Failed to load positions')
+ }
+ } catch (e) {
+ setError('Error loading positions')
+ }
+ setLoading(false)
+ }
+ fetchPositions()
+ }, [])
+
+ return (
+
+
+
+
+
Open Positions
+ {loading ?
Loading...
: error ?
{error}
: (
+
+
+
+ | Symbol |
+ Side |
+ Size |
+ Entry Price |
+ Unrealized PnL |
+
+
+
+ {positions.map((pos, i) => (
+
+ | {pos.symbol} |
+ {pos.side} |
+ {pos.size} |
+ {pos.entryPrice} |
+ {pos.unrealizedPnl} |
+
+ ))}
+
+
+ )}
+
+
+
+
+ )
+}
diff --git a/components/DeveloperSettings.tsx b/components/DeveloperSettings.tsx
new file mode 100644
index 0000000..a325504
--- /dev/null
+++ b/components/DeveloperSettings.tsx
@@ -0,0 +1,27 @@
+"use client"
+import React, { useState } from 'react'
+
+export default function DeveloperSettings() {
+ const [env, setEnv] = useState('')
+ const [message, setMessage] = useState('')
+
+ async function handleSave() {
+ // Example: Save env to localStorage or send to API
+ localStorage.setItem('devEnv', env)
+ setMessage('Settings saved!')
+ }
+
+ return (
+
+
Developer Settings
+
setEnv(e.target.value)}
+ />
+
+ {message &&
{message}
}
+
+ )
+}
diff --git a/components/TradingHistory.tsx b/components/TradingHistory.tsx
new file mode 100644
index 0000000..a701482
--- /dev/null
+++ b/components/TradingHistory.tsx
@@ -0,0 +1,60 @@
+"use client"
+import React, { useEffect, useState } from 'react'
+
+interface Trade {
+ id: string
+ symbol: string
+ side: string
+ amount: number
+ price: number
+ status: string
+ executedAt: string
+}
+
+export default function TradingHistory() {
+ const [trades, setTrades] = useState([])
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ async function fetchTrades() {
+ const res = await fetch('/api/trading-history')
+ if (res.ok) {
+ setTrades(await res.json())
+ }
+ setLoading(false)
+ }
+ fetchTrades()
+ }, [])
+
+ return (
+
+
Trading History
+ {loading ?
Loading...
: (
+
+
+
+ | Symbol |
+ Side |
+ Amount |
+ Price |
+ Status |
+ Executed At |
+
+
+
+ {trades.map(trade => (
+
+ | {trade.symbol} |
+ {trade.side} |
+ {trade.amount} |
+ {trade.price} |
+ {trade.status} |
+ {trade.executedAt} |
+
+ ))}
+
+
+ )}
+
+ )
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..c57ef07
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,20 @@
+version: '3.8'
+services:
+ app:
+ build: .
+ ports:
+ - "3000:3000"
+ volumes:
+ - ./:/app
+ - /app/node_modules
+ - ./screenshots:/app/screenshots
+ environment:
+ - NODE_ENV=development
+ - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
+ - PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
+ - TZ=Europe/Berlin
+ env_file:
+ - .env
+ # Uncomment for debugging
+ # command: ["npm", "run", "dev"]
+ # entrypoint: ["/bin/bash"]
diff --git a/lib/ai-analysis.ts b/lib/ai-analysis.ts
new file mode 100644
index 0000000..2acc651
--- /dev/null
+++ b/lib/ai-analysis.ts
@@ -0,0 +1,208 @@
+import OpenAI from 'openai'
+import fs from 'fs/promises'
+import path from 'path'
+
+const openai = new OpenAI({
+ apiKey: process.env.OPENAI_API_KEY,
+})
+
+export interface AnalysisResult {
+ summary: string
+ marketSentiment: 'BULLISH' | 'BEARISH' | 'NEUTRAL'
+ keyLevels: {
+ support: number[]
+ resistance: number[]
+ }
+ recommendation: 'BUY' | 'SELL' | 'HOLD'
+ confidence: number // 0-100
+ reasoning: string
+}
+
+export class AIAnalysisService {
+ async analyzeScreenshot(filename: string): Promise {
+ try {
+ const screenshotsDir = path.join(process.cwd(), 'screenshots')
+ const imagePath = path.join(screenshotsDir, filename)
+ // Read image file
+ const imageBuffer = await fs.readFile(imagePath)
+ const base64Image = imageBuffer.toString('base64')
+
+ const prompt = `You are now a professional trading assistant focused on short-term crypto trading using 5–15min timeframes. You behave with the precision and decisiveness of a top proprietary desk trader. No vagueness, no fluff.
+
+Analyze the attached TradingView chart screenshot and provide a detailed trading analysis.
+
+### WHEN GIVING A TRADE SETUP:
+Be 100% SPECIFIC. Provide:
+
+1. **ENTRY**
+ - Exact price level (with a ± entry buffer if needed)
+ - Rationale: e.g., "Rejection from 15 EMA + VWAP confluence near intraday supply"
+
+2. **STOP-LOSS (SL)**
+ - Exact level (not arbitrary)
+ - Explain *why* it's there: "Above VWAP + failed breakout zone"
+
+3. **TAKE PROFITS**
+ - TP1: Immediate structure (ex: previous low at $149.20)
+ - TP2: Extended target if momentum continues (e.g., $148.00)
+ - Mention **expected RSI/OBV behavior** at each TP zone
+
+4. **RISK-TO-REWARD**
+ - Show R:R. Ex: "1:2.5 — Risking $X to potentially gain $Y"
+
+5. **CONFIRMATION TRIGGER**
+ - Exact signal to wait for: e.g., "Bearish engulfing candle on rejection from VWAP zone"
+ - OBV: "Must be making lower highs + dropping below 30min average"
+ - RSI: "Should remain under 50 on rejection. Overbought ≥70 = wait"
+
+6. **INDICATOR ANALYSIS**
+ - **RSI**: If RSI crosses above 70 while price is under resistance → *wait*
+ - **VWAP**: If price retakes VWAP with bullish momentum → *consider invalidation*
+ - **OBV**: If OBV starts climbing while price stays flat → *early exit or reconsider bias*
+
+Return your answer as a JSON object with the following structure:
+{
+ "summary": "Brief market summary",
+ "marketSentiment": "BULLISH" | "BEARISH" | "NEUTRAL",
+ "keyLevels": {
+ "support": [number array],
+ "resistance": [number array]
+ },
+ "recommendation": "BUY" | "SELL" | "HOLD",
+ "confidence": number (0-100),
+ "reasoning": "Detailed reasoning with specific levels, indicators, and confirmation triggers"
+}
+
+Be concise but thorough. Only return valid JSON.`
+ const response = await openai.chat.completions.create({
+ model: "gpt-4o", // Updated to current vision model
+ messages: [
+ {
+ role: "user",
+ content: [
+ { type: "text", text: prompt },
+ { type: "image_url", image_url: { url: `data:image/png;base64,${base64Image}` } }
+ ]
+ }
+ ],
+ max_tokens: 1024
+ })
+ const content = response.choices[0]?.message?.content
+ if (!content) return null
+ // Extract JSON from response
+ const match = content.match(/\{[\s\S]*\}/)
+ if (!match) return null
+ const json = match[0]
+ const result = JSON.parse(json)
+ // Optionally: validate result structure here
+ return result as AnalysisResult
+ } catch (e) {
+ console.error('AI analysis error:', e)
+ return null
+ }
+ }
+
+ async analyzeMultipleScreenshots(filenames: string[]): Promise {
+ try {
+ const screenshotsDir = path.join(process.cwd(), 'screenshots')
+ const images: any[] = []
+
+ for (const filename of filenames) {
+ const imagePath = path.join(screenshotsDir, filename)
+ const imageBuffer = await fs.readFile(imagePath)
+ const base64Image = imageBuffer.toString('base64')
+ images.push({ type: "image_url", image_url: { url: `data:image/png;base64,${base64Image}` } })
+ }
+
+ const prompt = `You are now a professional trading assistant focused on short-term crypto trading using 5–15min timeframes. You behave with the precision and decisiveness of a top proprietary desk trader. No vagueness, no fluff.
+
+Analyze the attached TradingView chart screenshots (multiple layouts of the same symbol) and provide a comprehensive trading analysis by combining insights from all charts.
+
+### WHEN GIVING A TRADE SETUP:
+Be 100% SPECIFIC. Provide:
+
+1. **ENTRY**
+ - Exact price level (with a ± entry buffer if needed)
+ - Rationale: e.g., "Rejection from 15 EMA + VWAP confluence near intraday supply"
+
+2. **STOP-LOSS (SL)**
+ - Exact level (not arbitrary)
+ - Explain *why* it's there: "Above VWAP + failed breakout zone"
+
+3. **TAKE PROFITS**
+ - TP1: Immediate structure (ex: previous low at $149.20)
+ - TP2: Extended target if momentum continues (e.g., $148.00)
+ - Mention **expected RSI/OBV behavior** at each TP zone
+
+4. **RISK-TO-REWARD**
+ - Show R:R. Ex: "1:2.5 — Risking $X to potentially gain $Y"
+
+5. **CONFIRMATION TRIGGER**
+ - Exact signal to wait for: e.g., "Bearish engulfing candle on rejection from VWAP zone"
+ - OBV: "Must be making lower highs + dropping below 30min average"
+ - RSI: "Should remain under 50 on rejection. Overbought ≥70 = wait"
+
+6. **INDICATOR ANALYSIS**
+ - **RSI**: If RSI crosses above 70 while price is under resistance → *wait*
+ - **VWAP**: If price retakes VWAP with bullish momentum → *consider invalidation*
+ - **OBV**: If OBV starts climbing while price stays flat → *early exit or reconsider bias*
+
+Cross-reference all layouts to provide the most accurate analysis. If layouts show conflicting signals, explain which one takes priority and why.
+
+Return your answer as a JSON object with the following structure:
+{
+ "summary": "Brief market summary combining all layouts",
+ "marketSentiment": "BULLISH" | "BEARISH" | "NEUTRAL",
+ "keyLevels": {
+ "support": [number array],
+ "resistance": [number array]
+ },
+ "recommendation": "BUY" | "SELL" | "HOLD",
+ "confidence": number (0-100),
+ "reasoning": "Detailed reasoning with specific levels, indicators, and confirmation triggers from all layouts"
+}
+
+Be concise but thorough. Only return valid JSON.`
+
+ const response = await openai.chat.completions.create({
+ model: "gpt-4o",
+ messages: [
+ {
+ role: "user",
+ content: [
+ { type: "text", text: prompt },
+ ...images
+ ]
+ }
+ ],
+ max_tokens: 1500,
+ temperature: 0.1
+ })
+
+ const content = response.choices[0]?.message?.content
+ if (!content) {
+ throw new Error('No content received from OpenAI')
+ }
+
+ // Parse the JSON response
+ const jsonMatch = content.match(/\{[\s\S]*\}/)
+ if (!jsonMatch) {
+ throw new Error('No JSON found in response')
+ }
+
+ const analysis = JSON.parse(jsonMatch[0])
+
+ // Validate the structure
+ if (!analysis.summary || !analysis.marketSentiment || !analysis.recommendation || !analysis.confidence) {
+ throw new Error('Invalid analysis structure')
+ }
+
+ return analysis
+ } catch (error) {
+ console.error('AI multi-analysis error:', error)
+ return null
+ }
+ }
+}
+
+export const aiAnalysisService = new AIAnalysisService()
diff --git a/lib/auto-trading.ts b/lib/auto-trading.ts
new file mode 100644
index 0000000..48b6cbe
--- /dev/null
+++ b/lib/auto-trading.ts
@@ -0,0 +1,82 @@
+import { tradingViewCapture } from './tradingview'
+import { aiAnalysisService } from './ai-analysis'
+import prisma from './prisma'
+
+export interface AutoTradingConfig {
+ enabled: boolean
+ symbols: string[]
+ intervalMinutes: number
+ maxDailyTrades: number
+ tradingAmount: number
+ confidenceThreshold: number
+}
+
+export class AutoTradingService {
+ private config: AutoTradingConfig
+ private intervalId: NodeJS.Timeout | null = null
+ private dailyTradeCount: Record = {}
+
+ constructor(config: AutoTradingConfig) {
+ this.config = config
+ this.dailyTradeCount = {}
+ }
+
+ start() {
+ if (this.intervalId || !this.config.enabled) return
+ this.intervalId = setInterval(() => this.run(), this.config.intervalMinutes * 60 * 1000)
+ this.run() // Run immediately on start
+ }
+
+ stop() {
+ if (this.intervalId) {
+ clearInterval(this.intervalId)
+ this.intervalId = null
+ }
+ }
+
+ async run() {
+ if (!this.config.enabled) return
+ for (const symbol of this.config.symbols) {
+ if ((this.dailyTradeCount[symbol] || 0) >= this.config.maxDailyTrades) continue
+ // 1. Capture screenshot
+ const filename = `${symbol}_${Date.now()}.png`
+ const screenshotPath = await tradingViewCapture.capture(symbol, filename)
+ // 2. Analyze screenshot
+ const analysis = await aiAnalysisService.analyzeScreenshot(filename)
+ if (!analysis || analysis.confidence < this.config.confidenceThreshold) continue
+ // 3. Execute trade (stub: integrate with driftTradingService)
+ // const tradeResult = await driftTradingService.executeTrade({ ... })
+ // 4. Save trade to DB
+ await prisma.trade.create({
+ data: {
+ symbol,
+ side: analysis.recommendation === 'BUY' ? 'LONG' : analysis.recommendation === 'SELL' ? 'SHORT' : 'NONE',
+ amount: this.config.tradingAmount,
+ price: 0, // To be filled with actual execution price
+ status: 'PENDING',
+ screenshotUrl: screenshotPath,
+ aiAnalysis: JSON.stringify(analysis),
+ executedAt: new Date(),
+ userId: 'system', // Or actual user if available
+ }
+ })
+ this.dailyTradeCount[symbol] = (this.dailyTradeCount[symbol] || 0) + 1
+ }
+ }
+
+ setConfig(config: Partial) {
+ this.config = { ...this.config, ...config }
+ }
+}
+
+export function getAutoTradingService() {
+ // Singleton pattern or similar
+ return new AutoTradingService({
+ enabled: false,
+ symbols: ['BTCUSD'],
+ intervalMinutes: 15,
+ maxDailyTrades: 10,
+ tradingAmount: 100,
+ confidenceThreshold: 80
+ })
+}
diff --git a/lib/prisma.ts b/lib/prisma.ts
new file mode 100644
index 0000000..2d4761c
--- /dev/null
+++ b/lib/prisma.ts
@@ -0,0 +1,13 @@
+import { PrismaClient } from '@prisma/client'
+
+declare global {
+ var prisma: PrismaClient | undefined
+}
+
+const prisma = globalThis.prisma || new PrismaClient()
+
+if (process.env.NODE_ENV !== 'production') {
+ globalThis.prisma = prisma
+}
+
+export default prisma
diff --git a/lib/settings.ts b/lib/settings.ts
new file mode 100644
index 0000000..c3cb4f7
--- /dev/null
+++ b/lib/settings.ts
@@ -0,0 +1,88 @@
+import fs from 'fs/promises'
+import path from 'path'
+
+export interface TradingSettings {
+ symbol: string
+ timeframe: string
+ layouts: string[]
+ lastUpdated: number
+}
+
+const SETTINGS_FILE = path.join(process.cwd(), 'trading-settings.json')
+
+export class SettingsManager {
+ private static instance: SettingsManager
+ private settings: TradingSettings = {
+ symbol: 'BTCUSD',
+ timeframe: '5',
+ layouts: ['ai'],
+ lastUpdated: Date.now()
+ }
+
+ private constructor() {}
+
+ static getInstance(): SettingsManager {
+ if (!SettingsManager.instance) {
+ SettingsManager.instance = new SettingsManager()
+ }
+ return SettingsManager.instance
+ }
+
+ async loadSettings(): Promise {
+ try {
+ const data = await fs.readFile(SETTINGS_FILE, 'utf-8')
+ this.settings = JSON.parse(data)
+ console.log('Loaded settings:', this.settings)
+ } catch (error) {
+ console.log('No existing settings found, using defaults')
+ await this.saveSettings()
+ }
+ return this.settings
+ }
+
+ async saveSettings(): Promise {
+ try {
+ this.settings.lastUpdated = Date.now()
+ await fs.writeFile(SETTINGS_FILE, JSON.stringify(this.settings, null, 2))
+ console.log('Settings saved:', this.settings)
+ } catch (error) {
+ console.error('Failed to save settings:', error)
+ }
+ }
+
+ async updateSettings(updates: Partial): Promise {
+ this.settings = { ...this.settings, ...updates }
+ await this.saveSettings()
+ return this.settings
+ }
+
+ getSettings(): TradingSettings {
+ return this.settings
+ }
+
+ async setSymbol(symbol: string): Promise {
+ await this.updateSettings({ symbol })
+ }
+
+ async setTimeframe(timeframe: string): Promise {
+ await this.updateSettings({ timeframe })
+ }
+
+ async setLayouts(layouts: string[]): Promise {
+ await this.updateSettings({ layouts })
+ }
+
+ async addLayout(layout: string): Promise {
+ if (!this.settings.layouts.includes(layout)) {
+ const layouts = [...this.settings.layouts, layout]
+ await this.updateSettings({ layouts })
+ }
+ }
+
+ async removeLayout(layout: string): Promise {
+ const layouts = this.settings.layouts.filter(l => l !== layout)
+ await this.updateSettings({ layouts })
+ }
+}
+
+export const settingsManager = SettingsManager.getInstance()
diff --git a/lib/tradingview.ts b/lib/tradingview.ts
new file mode 100644
index 0000000..75a8b3d
--- /dev/null
+++ b/lib/tradingview.ts
@@ -0,0 +1,396 @@
+import puppeteer, { Browser, Page, Frame } from 'puppeteer'
+import path from 'path'
+import fs from 'fs/promises'
+import { settingsManager } from './settings'
+
+const TRADINGVIEW_EMAIL = process.env.TRADINGVIEW_EMAIL
+const TRADINGVIEW_PASSWORD = process.env.TRADINGVIEW_PASSWORD
+const TRADINGVIEW_LAYOUTS = (process.env.TRADINGVIEW_LAYOUTS || '').split(',').map(l => l.trim())
+const PUPPETEER_EXECUTABLE_PATH = process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium'
+
+export class TradingViewCapture {
+ private browser: Browser | null = null
+ private page: Page | null = null
+ private loggedIn = false
+
+ async init() {
+ if (!this.browser) {
+ this.browser = await puppeteer.launch({
+ headless: true,
+ args: [
+ '--no-sandbox',
+ '--disable-setuid-sandbox',
+ '--disable-dev-shm-usage',
+ '--disable-accelerated-2d-canvas',
+ '--no-first-run',
+ '--no-zygote',
+ '--disable-gpu'
+ ],
+ executablePath: PUPPETEER_EXECUTABLE_PATH
+ })
+ console.log('Puppeteer browser launched')
+ }
+ if (!this.page) {
+ this.page = await this.browser.newPage()
+ await this.page.setViewport({ width: 1920, height: 1080 })
+ console.log('Puppeteer page created')
+ }
+ if (!this.loggedIn) {
+ console.log('Logging in to TradingView...')
+ await this.login()
+ this.loggedIn = true
+ console.log('Logged in to TradingView')
+ }
+ return this.page
+ }
+
+ async login() {
+ if (!TRADINGVIEW_EMAIL || !TRADINGVIEW_PASSWORD) {
+ throw new Error('TradingView credentials not set in .env')
+ }
+ const page = this.page || (await this.browser!.newPage())
+ console.log('Navigating to TradingView login page...')
+ await page.goto('https://www.tradingview.com/#signin', { waitUntil: 'networkidle2' })
+
+ // Check if we're already logged in
+ try {
+ const loggedInIndicator = await page.waitForSelector('.tv-header__user-menu-button, [data-name="header-user-menu"]', { timeout: 3000 })
+ if (loggedInIndicator) {
+ console.log('Already logged in to TradingView')
+ return
+ }
+ } catch (e) {
+ console.log('Not logged in yet, proceeding with login...')
+ }
+
+ try {
+ // Wait for the login modal to appear and look for email input directly
+ console.log('Looking for email input field...')
+
+ // Try to find the email input field directly (new TradingView layout)
+ const emailInput = await page.waitForSelector('input[name="username"], input[name="email"], input[type="email"], input[placeholder*="email" i]', { timeout: 10000 })
+
+ if (emailInput) {
+ console.log('Found email input field directly')
+ await emailInput.click() // Click to focus
+ await emailInput.type(TRADINGVIEW_EMAIL, { delay: 50 })
+
+ // Find password field
+ const passwordInput = await page.waitForSelector('input[name="password"], input[type="password"], input[placeholder*="password" i]', { timeout: 5000 })
+ if (!passwordInput) {
+ throw new Error('Could not find password input field')
+ }
+ await passwordInput.click() // Click to focus
+ await passwordInput.type(TRADINGVIEW_PASSWORD, { delay: 50 })
+
+ // Find and click the sign in button
+ const signInButton = await page.waitForSelector('button[type="submit"]', { timeout: 5000 })
+ if (!signInButton) {
+ // Try to find button with sign in text
+ const buttons = await page.$$('button')
+ let foundButton = null
+ for (const btn of buttons) {
+ const text = await page.evaluate(el => el.innerText || el.textContent, btn)
+ if (text && (text.toLowerCase().includes('sign in') || text.toLowerCase().includes('login'))) {
+ foundButton = btn
+ break
+ }
+ }
+ if (!foundButton) {
+ throw new Error('Could not find sign in button')
+ }
+ await foundButton.click()
+ } else {
+ await signInButton.click()
+ }
+ } else {
+ throw new Error('Could not find email input field')
+ }
+ } catch (e) {
+ // Fallback: try to find email button first
+ console.log('Fallback: looking for email button...')
+ try {
+ await page.waitForSelector('button', { timeout: 15000 })
+ const buttons = await page.$$('button')
+ let emailBtn = null
+
+ // Look for email button with various text patterns
+ for (const btn of buttons) {
+ const text = await page.evaluate(el => el.innerText || el.textContent, btn)
+ if (text && (
+ text.trim().toLowerCase().includes('email') ||
+ text.trim().toLowerCase().includes('sign in with email') ||
+ text.trim().toLowerCase().includes('continue with email')
+ )) {
+ emailBtn = btn
+ break
+ }
+ }
+
+ if (emailBtn) {
+ console.log('Found email button, clicking...')
+ await emailBtn.click()
+ await new Promise(res => setTimeout(res, 1000))
+
+ // Now fill in the form
+ const emailInput = await page.waitForSelector('input[name="username"], input[name="email"], input[type="email"], input[placeholder*="email" i]', { timeout: 10000 })
+ if (!emailInput) {
+ throw new Error('Could not find email input field after clicking email button')
+ }
+ await emailInput.click() // Click to focus
+ await emailInput.type(TRADINGVIEW_EMAIL, { delay: 50 })
+
+ const passwordInput = await page.waitForSelector('input[name="password"], input[type="password"], input[placeholder*="password" i]', { timeout: 5000 })
+ if (!passwordInput) {
+ throw new Error('Could not find password input field after clicking email button')
+ }
+ await passwordInput.click() // Click to focus
+ await passwordInput.type(TRADINGVIEW_PASSWORD, { delay: 50 })
+
+ const signInButton = await page.waitForSelector('button[type="submit"]', { timeout: 5000 })
+ if (!signInButton) {
+ // Try to find button with sign in text
+ const buttons = await page.$$('button')
+ let foundButton = null
+ for (const btn of buttons) {
+ const text = await page.evaluate(el => el.innerText || el.textContent, btn)
+ if (text && (text.toLowerCase().includes('sign in') || text.toLowerCase().includes('login'))) {
+ foundButton = btn
+ break
+ }
+ }
+ if (!foundButton) {
+ throw new Error('Could not find sign in button after clicking email button')
+ }
+ await foundButton.click()
+ } else {
+ await signInButton.click()
+ }
+ } else {
+ throw new Error('Could not find email button')
+ }
+ } catch (e2) {
+ console.error('Could not find or click email button:', e2)
+ const errorMessage = e2 instanceof Error ? e2.message : String(e2)
+ throw new Error('Could not find or click email button on TradingView login page. ' + errorMessage)
+ }
+ }
+
+ // Wait for navigation or dashboard (main page)
+ try {
+ console.log('Waiting for login to complete...')
+ await page.waitForSelector('.tv-header__user-menu-button, .chart-container, [data-name="header-user-menu"]', { timeout: 30000 })
+ } catch (e) {
+ console.error('Login navigation did not complete.')
+ throw new Error('Login navigation did not complete.')
+ }
+
+ console.log('TradingView login complete')
+ }
+
+ async capture(symbol: string, filename: string, layouts?: string[], timeframe?: string) {
+ console.log('Working directory:', process.cwd())
+
+ // Load settings and update if provided
+ const settings = await settingsManager.loadSettings()
+ if (symbol && symbol !== settings.symbol) {
+ await settingsManager.setSymbol(symbol)
+ }
+ if (timeframe && timeframe !== settings.timeframe) {
+ await settingsManager.setTimeframe(timeframe)
+ }
+ if (layouts && JSON.stringify(layouts) !== JSON.stringify(settings.layouts)) {
+ await settingsManager.setLayouts(layouts)
+ }
+
+ // Use saved settings if not provided
+ const finalSymbol = symbol || settings.symbol
+ const finalTimeframe = timeframe || settings.timeframe
+ const finalLayouts = layouts || settings.layouts
+
+ console.log('Using settings:', { symbol: finalSymbol, timeframe: finalTimeframe, layouts: finalLayouts })
+
+ const page = await this.init()
+ // Add timeframe to TradingView URL if provided
+ let url = `https://www.tradingview.com/chart/?symbol=${finalSymbol}`
+ if (finalTimeframe) {
+ url += `&interval=${encodeURIComponent(finalTimeframe)}`
+ }
+ try {
+ console.log('Navigating to TradingView chart:', url)
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 })
+ console.log('Successfully navigated to chart')
+ } catch (e: any) {
+ console.error('Failed to load TradingView chart page:', e)
+ throw new Error('Failed to load TradingView chart page: ' + (e.message || e))
+ }
+
+ // Capture screenshots for each layout
+ const screenshots: string[] = []
+
+ for (let i = 0; i < finalLayouts.length; i++) {
+ const layout = finalLayouts[i]
+ console.log(`Processing layout ${i + 1}/${finalLayouts.length}: ${layout}`)
+
+ // Load the layout
+ await this.loadLayout(page, layout)
+
+ // Wait for layout to load
+ await new Promise(res => setTimeout(res, 3000))
+
+ // Generate filename for this layout
+ const layoutFilename = filename.replace('.png', `_${layout}.png`)
+ const screenshotsDir = path.join(process.cwd(), 'screenshots')
+ await fs.mkdir(screenshotsDir, { recursive: true })
+ const filePath = path.join(screenshotsDir, layoutFilename)
+
+ try {
+ await page.screenshot({ path: filePath as `${string}.png`, type: 'png' })
+ console.log(`Screenshot saved for layout ${layout}:`, filePath)
+ screenshots.push(filePath)
+ } catch (e: any) {
+ const debugScreenshotErrorPath = path.resolve(`debug_screenshot_error_${layout}.png`) as `${string}.png`
+ await page.screenshot({ path: debugScreenshotErrorPath })
+ console.error(`Failed to capture screenshot for layout ${layout}:`, e)
+ console.error('Screenshot on screenshot error:', debugScreenshotErrorPath)
+ throw new Error(`Failed to capture screenshot for layout ${layout}: ` + (e.message || e))
+ }
+ }
+
+ return screenshots
+ }
+
+ private async loadLayout(page: Page, layout: string): Promise {
+ try {
+ console.log('Trying to load layout:', layout)
+
+ // Try multiple selectors for the layout button
+ const layoutSelectors = [
+ '[data-name="load-chart-layout-dialog"]',
+ '[data-name="layouts-menu"]',
+ '[data-name="chart-layout-button"]',
+ 'button[title*="Layout" i]',
+ 'button[aria-label*="Layout" i]',
+ '[data-testid*="layout"]'
+ ]
+
+ let layoutButton = null
+ for (const selector of layoutSelectors) {
+ try {
+ layoutButton = await page.waitForSelector(selector, { timeout: 3000 })
+ if (layoutButton) {
+ console.log('Found layout button with selector:', selector)
+ break
+ }
+ } catch (e) {
+ // Continue to next selector
+ }
+ }
+
+ if (!layoutButton) {
+ // Try to find layout button by text content
+ const buttons = await page.$$('button, [role="button"]')
+ for (const btn of buttons) {
+ const text = await page.evaluate(el => {
+ const element = el as HTMLElement
+ return element.innerText || element.textContent || element.title || element.getAttribute('aria-label')
+ }, btn)
+ if (text && text.toLowerCase().includes('layout')) {
+ layoutButton = btn
+ console.log('Found layout button by text:', text)
+ break
+ }
+ }
+ }
+
+ if (layoutButton) {
+ await layoutButton.click()
+ console.log('Clicked layout button')
+ await new Promise(res => setTimeout(res, 1000))
+
+ // Look for search input or layout items
+ const searchSelectors = [
+ 'input[name="search"]',
+ 'input[placeholder*="search" i]',
+ 'input[type="search"]',
+ 'input[data-name="search"]'
+ ]
+
+ let searchInput = null
+ for (const selector of searchSelectors) {
+ try {
+ searchInput = await page.waitForSelector(selector, { timeout: 3000 })
+ if (searchInput) break
+ } catch (e) {
+ // Continue to next selector
+ }
+ }
+
+ if (searchInput) {
+ console.log('Found search input, typing layout name...')
+ await searchInput.type(layout, { delay: 50 })
+ await new Promise(res => setTimeout(res, 2000))
+
+ // Try to find and click the layout item
+ const layoutItemSelectors = [
+ '[data-name="chart-layout-list-item"]',
+ '[data-testid*="layout-item"]',
+ '.layout-item',
+ '[role="option"]'
+ ]
+
+ let layoutItem = null
+ for (const selector of layoutItemSelectors) {
+ try {
+ const items = await page.$$(selector)
+ for (const item of items) {
+ const text = await page.evaluate(el => {
+ const element = el as HTMLElement
+ return element.innerText || element.textContent
+ }, item)
+ if (text && text.toLowerCase().includes(layout.toLowerCase())) {
+ layoutItem = item
+ break
+ }
+ }
+ if (layoutItem) break
+ } catch (e) {
+ // Continue to next selector
+ }
+ }
+
+ if (layoutItem) {
+ await layoutItem.click()
+ console.log('Clicked layout item:', layout)
+ } else {
+ console.log('Layout item not found, trying generic approach...')
+ await page.evaluate((layout) => {
+ const items = Array.from(document.querySelectorAll('*'))
+ const item = items.find(el => el.textContent?.toLowerCase().includes(layout.toLowerCase()))
+ if (item) (item as HTMLElement).click()
+ }, layout)
+ }
+ } else {
+ console.log('Search input not found, trying to find layout directly...')
+ await page.evaluate((layout) => {
+ const items = Array.from(document.querySelectorAll('*'))
+ const item = items.find(el => el.textContent?.toLowerCase().includes(layout.toLowerCase()))
+ if (item) (item as HTMLElement).click()
+ }, layout)
+ }
+ await new Promise(res => setTimeout(res, 4000))
+ console.log('Layout loaded:', layout)
+ } else {
+ console.log('Layout button not found, skipping layout loading')
+ }
+ } catch (e: any) {
+ const debugLayoutErrorPath = path.resolve('debug_layout_error.png') as `${string}.png`
+ await page.screenshot({ path: debugLayoutErrorPath })
+ console.error('TradingView layout not found or could not be loaded:', e)
+ console.log('Continuing without layout...')
+ // Don't throw error, just continue without layout
+ }
+ }
+}
+
+export const tradingViewCapture = new TradingViewCapture()
diff --git a/package-lock.json b/package-lock.json
index 4cf5b4b..6327ccd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@drift-labs/sdk": "^2.126.0-beta.14",
"@prisma/client": "^6.11.1",
"@solana/web3.js": "^1.98.2",
+ "bs58": "^6.0.0",
"next": "15.3.5",
"openai": "^5.8.3",
"prisma": "^6.11.1",
@@ -148,6 +149,15 @@
"@solana/web3.js": "^1.68.0"
}
},
+ "node_modules/@coral-xyz/anchor-30/node_modules/bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "license": "MIT",
+ "dependencies": {
+ "base-x": "^3.0.2"
+ }
+ },
"node_modules/@coral-xyz/anchor-errors": {
"version": "0.30.1",
"resolved": "https://registry.npmjs.org/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz",
@@ -156,6 +166,16 @@
"node": ">=10"
}
},
+ "node_modules/@coral-xyz/anchor/node_modules/bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "base-x": "^3.0.2"
+ }
+ },
"node_modules/@coral-xyz/borsh": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.28.0.tgz",
@@ -294,6 +314,15 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@drift-labs/sdk/node_modules/bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "license": "MIT",
+ "dependencies": {
+ "base-x": "^3.0.2"
+ }
+ },
"node_modules/@drift-labs/sdk/node_modules/nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
@@ -1509,6 +1538,15 @@
"@solana/web3.js": "^1.95.5"
}
},
+ "node_modules/@openbook-dex/openbook-v2/node_modules/bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "license": "MIT",
+ "dependencies": {
+ "base-x": "^3.0.2"
+ }
+ },
"node_modules/@prisma/client": {
"version": "6.11.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.11.1.tgz",
@@ -1602,6 +1640,15 @@
"node": ">=11"
}
},
+ "node_modules/@project-serum/anchor/node_modules/bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "license": "MIT",
+ "dependencies": {
+ "base-x": "^3.0.2"
+ }
+ },
"node_modules/@project-serum/anchor/node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@@ -1807,6 +1854,15 @@
"@solana/web3.js": "^1.68.0"
}
},
+ "node_modules/@pythnetwork/pyth-solana-receiver/node_modules/bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "license": "MIT",
+ "dependencies": {
+ "base-x": "^3.0.2"
+ }
+ },
"node_modules/@pythnetwork/solana-utils": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@pythnetwork/solana-utils/-/solana-utils-0.4.5.tgz",
@@ -2333,6 +2389,15 @@
"@types/node": "*"
}
},
+ "node_modules/@solana/web3.js/node_modules/bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "license": "MIT",
+ "dependencies": {
+ "base-x": "^3.0.2"
+ }
+ },
"node_modules/@solana/web3.js/node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
@@ -2401,19 +2466,6 @@
"node": ">=20"
}
},
- "node_modules/@switchboard-xyz/common/node_modules/base-x": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
- "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="
- },
- "node_modules/@switchboard-xyz/common/node_modules/bs58": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
- "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
- "dependencies": {
- "base-x": "^5.0.0"
- }
- },
"node_modules/@switchboard-xyz/common/node_modules/js-sha256": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz",
@@ -2439,19 +2491,6 @@
"@switchboard-xyz/common": ">=3.0.0"
}
},
- "node_modules/@switchboard-xyz/on-demand/node_modules/base-x": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
- "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="
- },
- "node_modules/@switchboard-xyz/on-demand/node_modules/bs58": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
- "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
- "dependencies": {
- "base-x": "^5.0.0"
- }
- },
"node_modules/@tailwindcss/node": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
@@ -3841,6 +3880,15 @@
"text-encoding-utf-8": "^1.0.2"
}
},
+ "node_modules/borsh/node_modules/bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "license": "MIT",
+ "dependencies": {
+ "base-x": "^3.0.2"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -3864,13 +3912,20 @@
}
},
"node_modules/bs58": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
- "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
+ "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
+ "license": "MIT",
"dependencies": {
- "base-x": "^3.0.2"
+ "base-x": "^5.0.0"
}
},
+ "node_modules/bs58/node_modules/base-x": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
+ "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==",
+ "license": "MIT"
+ },
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -6306,6 +6361,15 @@
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
"integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ=="
},
+ "node_modules/jito-ts/node_modules/bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "license": "MIT",
+ "dependencies": {
+ "base-x": "^3.0.2"
+ }
+ },
"node_modules/jito-ts/node_modules/superstruct": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz",
@@ -8173,6 +8237,16 @@
"node": ">= 10"
}
},
+ "node_modules/solana-bankrun/node_modules/bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "base-x": "^3.0.2"
+ }
+ },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
diff --git a/package.json b/package.json
index f7757fd..ab13ee8 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"@drift-labs/sdk": "^2.126.0-beta.14",
"@prisma/client": "^6.11.1",
"@solana/web3.js": "^1.98.2",
+ "bs58": "^6.0.0",
"next": "15.3.5",
"openai": "^5.8.3",
"prisma": "^6.11.1",
diff --git a/screenshots/BTCUSD_ai_5_1752062666039.png b/screenshots/BTCUSD_ai_5_1752062666039.png
new file mode 100644
index 0000000..73545ae
Binary files /dev/null and b/screenshots/BTCUSD_ai_5_1752062666039.png differ
diff --git a/screenshots/SOLUSD_ai_5_1752062839851.png b/screenshots/SOLUSD_ai_5_1752062839851.png
new file mode 100644
index 0000000..6ca612a
Binary files /dev/null and b/screenshots/SOLUSD_ai_5_1752062839851.png differ
diff --git a/scripts/convert-solana-key.js b/scripts/convert-solana-key.js
new file mode 100644
index 0000000..3fdedf7
--- /dev/null
+++ b/scripts/convert-solana-key.js
@@ -0,0 +1,11 @@
+const bs58 = require('bs58');
+const fs = require('fs');
+
+// Paste your base58 private key string here
+const base58 = process.argv[2];
+if (!base58) {
+ console.error('Usage: node convert.js ');
+ process.exit(1);
+}
+const arr = Array.from(bs58.decode(base58));
+console.log(JSON.stringify(arr));
diff --git a/scripts/convert-solana-key.mjs b/scripts/convert-solana-key.mjs
new file mode 100644
index 0000000..c331483
--- /dev/null
+++ b/scripts/convert-solana-key.mjs
@@ -0,0 +1,11 @@
+import bs58 from 'bs58';
+import fs from 'fs';
+
+// Paste your base58 private key string here
+const base58 = process.argv[2];
+if (!base58) {
+ console.error('Usage: node convert-solana-key.mjs ');
+ process.exit(1);
+}
+const arr = Array.from(bs58.decode(base58));
+console.log(JSON.stringify(arr));