diff --git a/api/handler_ai_cost.go b/api/handler_ai_cost.go new file mode 100644 index 00000000..598cef55 --- /dev/null +++ b/api/handler_ai_cost.go @@ -0,0 +1,43 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// handleGetAICosts returns AI charges for a specific trader +func (s *Server) handleGetAICosts(c *gin.Context) { + traderID := c.Query("trader_id") + period := c.DefaultQuery("period", "today") + + if traderID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "trader_id is required"}) + return + } + + charges, total, err := s.store.AICharge().GetCharges(traderID, period) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "charges": charges, + "total": total, + "count": len(charges), + }) +} + +// handleGetAICostsSummary returns AI cost summary across all traders +func (s *Server) handleGetAICostsSummary(c *gin.Context) { + period := c.DefaultQuery("period", "today") + + total, count, byModel := s.store.AICharge().GetSummary(period) + + c.JSON(http.StatusOK, gin.H{ + "total": total, + "count": count, + "by_model": byModel, + }) +} diff --git a/api/handler_wallet.go b/api/handler_wallet.go index 7c430696..dca9a1bf 100644 --- a/api/handler_wallet.go +++ b/api/handler_wallet.go @@ -1,13 +1,10 @@ package api import ( - "bytes" "encoding/hex" - "encoding/json" "fmt" - "io" - "math/big" "net/http" + "nofx/wallet" "strings" "time" @@ -27,11 +24,7 @@ type walletValidateResponse struct { Error string `json:"error,omitempty"` } -const ( - baseRPCURL = "https://mainnet.base.org" - usdcContractBase = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" - usdcDecimals = 6 -) + func (s *Server) handleWalletValidate(c *gin.Context) { var req walletValidateRequest @@ -85,7 +78,7 @@ func (s *Server) handleWalletValidate(c *gin.Context) { addrHex := address.Hex() // Query USDC balance (async-ish, but sequential for simplicity) - balanceStr := queryUSDCBalance(addrHex) + balanceStr := wallet.QueryUSDCBalanceStr(addrHex) // Check claw402 health claw402Status := checkClaw402Health() @@ -98,65 +91,28 @@ func (s *Server) handleWalletValidate(c *gin.Context) { }) } -func queryUSDCBalance(address string) string { - // Build balanceOf(address) call data - // Function selector: 0x70a08231 - // Pad address to 32 bytes - addrNoPre := strings.TrimPrefix(strings.ToLower(address), "0x") - data := "0x70a08231" + fmt.Sprintf("%064s", addrNoPre) - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "method": "eth_call", - "params": []interface{}{ - map[string]string{ - "to": usdcContractBase, - "data": data, - }, - "latest", - }, - "id": 1, - } - body, err := json.Marshal(payload) +type walletGenerateResponse struct { + Address string `json:"address"` + PrivateKey string `json:"private_key"` +} + +func (s *Server) handleWalletGenerate(c *gin.Context) { + // Generate new EVM wallet + privateKey, err := crypto.GenerateKey() if err != nil { - return "0.00" + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate wallet"}) + return } - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Post(baseRPCURL, "application/json", bytes.NewReader(body)) - if err != nil { - return "0.00" - } - defer resp.Body.Close() + address := crypto.PubkeyToAddress(privateKey.PublicKey) + privKeyHex := "0x" + hex.EncodeToString(crypto.FromECDSA(privateKey)) - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return "0.00" - } - - var rpcResp struct { - Result string `json:"result"` - } - if err := json.Unmarshal(respBody, &rpcResp); err != nil { - return "0.00" - } - - // Parse hex result - hexStr := strings.TrimPrefix(rpcResp.Result, "0x") - if hexStr == "" || hexStr == "0" { - return "0.00" - } - - balance := new(big.Int) - balance.SetString(hexStr, 16) - - // Convert to float with 6 decimals - divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(usdcDecimals), nil) - whole := new(big.Int).Div(balance, divisor) - remainder := new(big.Int).Mod(balance, divisor) - - return fmt.Sprintf("%d.%06d", whole, remainder) + c.JSON(http.StatusOK, walletGenerateResponse{ + Address: address.Hex(), + PrivateKey: privKeyHex, + }) } func checkClaw402Health() string { diff --git a/api/server.go b/api/server.go index f2ea3ab5..a6037e77 100644 --- a/api/server.go +++ b/api/server.go @@ -89,6 +89,7 @@ func (s *Server) setupRoutes() { // Wallet validation (no authentication required — used by frontend config form) api.POST("/wallet/validate", s.handleWalletValidate) + api.POST("/wallet/generate", s.handleWalletGenerate) // Crypto related endpoints (no authentication required, not exposed to bot) api.GET("/crypto/config", s.cryptoHandler.HandleGetCryptoConfig) @@ -173,6 +174,10 @@ Body: {"show_in_competition":}`, `:id = trader_id from GET /api/my-traders.`, s.handleGetGridRiskInfo) + // AI cost tracking + s.route(protected, "GET", "/ai-costs", "Get AI call costs for a trader (?trader_id=xxx&period=today)", s.handleGetAICosts) + s.route(protected, "GET", "/ai-costs/summary", "Get AI cost summary (?period=today)", s.handleGetAICostsSummary) + // AI model configuration s.routeWithSchema(protected, "GET", "/models", "List AI model configs", `Returns: [{"id":"","name":"","provider":"","enabled":}] diff --git a/store/ai_charge.go b/store/ai_charge.go new file mode 100644 index 00000000..99ff6301 --- /dev/null +++ b/store/ai_charge.go @@ -0,0 +1,167 @@ +package store + +import ( + "time" + + "gorm.io/gorm" +) + +// AICharge represents a single AI call charge record +type AICharge struct { + ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` + TraderID string `gorm:"column:trader_id;not null;index:idx_ai_charges_trader" json:"trader_id"` + Model string `gorm:"column:model;not null" json:"model"` + Provider string `gorm:"column:provider;not null" json:"provider"` + CostUSD float64 `gorm:"column:cost_usd;not null" json:"cost_usd"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` +} + +func (AICharge) TableName() string { return "ai_charges" } + +// modelPrices maps model ID to approximate cost per call in USD +var modelPrices = map[string]float64{ + "deepseek": 0.003, + "deepseek-reasoner": 0.005, + "gpt-5.4": 0.05, + "gpt-5.4-pro": 0.50, + "gpt-5.3": 0.01, + "gpt-5-mini": 0.005, + "claude-opus": 0.12, + "qwen-max": 0.01, + "qwen-plus": 0.005, + "qwen-turbo": 0.002, + "qwen-flash": 0.002, + "grok-4.1": 0.06, + "gemini-3.1-pro": 0.03, + "kimi-k2.5": 0.008, +} + +// GetModelPrice returns the price per call for a given model +func GetModelPrice(model string) float64 { + if price, ok := modelPrices[model]; ok { + return price + } + return 0.01 // default fallback +} + +// AIChargeStore handles AI charge records +type AIChargeStore struct { + db *gorm.DB +} + +// NewAIChargeStore creates a new AIChargeStore +func NewAIChargeStore(db *gorm.DB) *AIChargeStore { + return &AIChargeStore{db: db} +} + +func (s *AIChargeStore) initTables() error { + return s.db.AutoMigrate(&AICharge{}) +} + +// Record records a new AI charge +func (s *AIChargeStore) Record(traderID, model, provider string) error { + cost := GetModelPrice(model) + charge := &AICharge{ + TraderID: traderID, + Model: model, + Provider: provider, + CostUSD: cost, + } + return s.db.Create(charge).Error +} + +// GetCharges returns charges for a trader within a period, plus total cost +func (s *AIChargeStore) GetCharges(traderID string, period string) ([]AICharge, float64, error) { + var charges []AICharge + query := s.db.Where("trader_id = ?", traderID) + query = applyPeriodFilter(query, period) + if err := query.Order("created_at DESC").Find(&charges).Error; err != nil { + return nil, 0, err + } + + var total float64 + for _, c := range charges { + total += c.CostUSD + } + return charges, total, nil +} + +// GetDailyCost returns total cost across all traders for a period +func (s *AIChargeStore) GetDailyCost(period string) float64 { + var total float64 + query := s.db.Model(&AICharge{}).Select("COALESCE(SUM(cost_usd), 0)") + query = applyPeriodFilter(query, period) + query.Scan(&total) + return total +} + +// GetSummary returns summary stats for a period +func (s *AIChargeStore) GetSummary(period string) (total float64, count int64, byModel map[string]float64) { + byModel = make(map[string]float64) + + query := s.db.Model(&AICharge{}) + query = applyPeriodFilter(query, period) + query.Count(&count) + + query2 := s.db.Model(&AICharge{}).Select("COALESCE(SUM(cost_usd), 0)") + query2 = applyPeriodFilter(query2, period) + query2.Scan(&total) + + // By model breakdown + type modelCost struct { + Model string `gorm:"column:model"` + Total float64 `gorm:"column:total"` + } + var results []modelCost + query3 := s.db.Model(&AICharge{}).Select("model, SUM(cost_usd) as total").Group("model") + query3 = applyPeriodFilter(query3, period) + query3.Find(&results) + for _, r := range results { + byModel[r.Model] = r.Total + } + + return total, count, byModel +} + +func applyPeriodFilter(query *gorm.DB, period string) *gorm.DB { + now := time.Now() + switch period { + case "today": + start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + return query.Where("created_at >= ?", start) + case "week": + return query.Where("created_at >= ?", now.AddDate(0, 0, -7)) + case "month": + return query.Where("created_at >= ?", now.AddDate(0, -1, 0)) + case "all": + return query + default: + // Try parse as date + if t, err := time.Parse("2006-01-02", period); err == nil { + end := t.AddDate(0, 0, 1) + return query.Where("created_at >= ? AND created_at < ?", t, end) + } + // Default to today + start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + return query.Where("created_at >= ?", start) + } +} + +// IsClaw402Config checks if a trader config uses claw402 payment provider +func IsClaw402Config(aiModel string) bool { + return aiModel == "claw402" +} + +// EstimateRunway estimates how many days the given USDC balance will last +func EstimateRunway(usdcBalance float64, modelName string, scanIntervalMinutes int) (dailyCost float64, runwayDays float64) { + if scanIntervalMinutes <= 0 { + scanIntervalMinutes = 3 + } + callsPerDay := float64(24*60) / float64(scanIntervalMinutes) + pricePerCall := GetModelPrice(modelName) + dailyCost = callsPerDay * pricePerCall + if dailyCost > 0 && usdcBalance > 0 { + runwayDays = usdcBalance / dailyCost + } + return dailyCost, runwayDays +} diff --git a/store/store.go b/store/store.go index 9cd1645e..516fb75a 100644 --- a/store/store.go +++ b/store/store.go @@ -28,6 +28,7 @@ type Store struct { equity *EquityStore order *OrderStore grid *GridStore + aiCharge *AIChargeStore telegramConfig TelegramConfigStore mu sync.RWMutex @@ -160,6 +161,9 @@ func (s *Store) initTables() error { if err := s.TelegramConfig().(*telegramConfigStore).initTables(); err != nil { return fmt.Errorf("failed to initialize telegram config tables: %w", err) } + if err := s.AICharge().initTables(); err != nil { + return fmt.Errorf("failed to initialize AI charge tables: %w", err) + } return nil } @@ -283,6 +287,16 @@ func (s *Store) Grid() *GridStore { return s.grid } +// AICharge gets AI charge storage +func (s *Store) AICharge() *AIChargeStore { + s.mu.Lock() + defer s.mu.Unlock() + if s.aiCharge == nil { + s.aiCharge = NewAIChargeStore(s.gdb) + } + return s.aiCharge +} + // TelegramConfig gets Telegram bot configuration storage func (s *Store) TelegramConfig() TelegramConfigStore { s.mu.Lock() diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 6c958509..7f755b68 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -8,6 +8,8 @@ import ( _ "nofx/mcp/payment" _ "nofx/mcp/provider" "nofx/store" + "nofx/wallet" + "github.com/ethereum/go-ethereum/crypto" "nofx/trader/aster" "nofx/trader/binance" "nofx/trader/bitget" @@ -145,6 +147,7 @@ type AutoTrader struct { lastBalanceSyncTime time.Time // Last balance sync time userID string // User ID gridState *GridState // Grid trading state (only used when StrategyType == "grid_trading") + claw402WalletAddr string // Claw402 wallet address (derived from private key at start) } // NewAutoTrader creates an automatic trader @@ -371,6 +374,9 @@ func (at *AutoTrader) Run() error { logger.Infof("💰 Initial balance: %.2f USDT", at.initialBalance) logger.Infof("⚙️ Scan interval: %v", at.config.ScanInterval) logger.Info("🤖 AI will make full decisions on leverage, position size, stop loss/take profit, etc.") + + // Pre-launch checks for claw402 users + at.runPreLaunchChecks() at.monitorWg.Add(1) defer at.monitorWg.Done() @@ -587,3 +593,63 @@ func calculatePnLPercentage(unrealizedPnl, marginUsed float64) float64 { } return 0.0 } + +// runPreLaunchChecks performs pre-launch checks for claw402 users (wallet balance, runway estimate) +func (at *AutoTrader) runPreLaunchChecks() { + if !store.IsClaw402Config(at.config.AIModel) { + return + } + + logger.Info("🔍 Running pre-launch checks (claw402)...") + + // Derive wallet address from CustomAPIKey (which is the private key for claw402) + if at.config.CustomAPIKey != "" { + // Try to derive address using go-ethereum + addr := deriveWalletAddress(at.config.CustomAPIKey) + if addr != "" { + at.claw402WalletAddr = addr + logger.Infof("💳 [%s] Claw402 wallet: %s", at.name, addr) + + // Query USDC balance + balance, err := wallet.QueryUSDCBalance(addr) + if err != nil { + logger.Warnf("⚠️ [%s] Could not query USDC balance: %v", at.name, err) + } else { + // Estimate runway + scanMinutes := int(at.config.ScanInterval.Minutes()) + modelName := at.config.CustomModelName + if modelName == "" { + modelName = "deepseek" + } + dailyCost, runway := store.EstimateRunway(balance, modelName, scanMinutes) + logger.Infof("💰 [%s] USDC Balance: $%.2f | Daily AI cost: ~$%.2f | Runway: ~%.1f days", + at.name, balance, dailyCost, runway) + + if balance < 1.0 { + logger.Warnf("⚠️ [%s] Low USDC balance! Consider topping up.", at.name) + } + if balance <= 0 { + logger.Errorf("🚨 [%s] USDC balance is ZERO — AI calls will fail!", at.name) + } + } + } + } + + logger.Info("✅ Pre-launch checks complete") +} + +// deriveWalletAddress derives an Ethereum address from a hex private key +func deriveWalletAddress(privateKeyHex string) string { + // Remove 0x prefix if present + if len(privateKeyHex) > 2 && privateKeyHex[:2] == "0x" { + privateKeyHex = privateKeyHex[2:] + } + + privateKey, err := crypto.HexToECDSA(privateKeyHex) + if err != nil { + return "" + } + + address := crypto.PubkeyToAddress(privateKey.PublicKey) + return address.Hex() +} diff --git a/trader/auto_trader_loop.go b/trader/auto_trader_loop.go index fe1cedcd..0242e3bf 100644 --- a/trader/auto_trader_loop.go +++ b/trader/auto_trader_loop.go @@ -6,6 +6,7 @@ import ( "nofx/kernel" "nofx/logger" "nofx/store" + "nofx/wallet" "strings" "time" ) @@ -27,6 +28,11 @@ func (at *AutoTrader) runCycle() error { return nil } + // Check USDC balance periodically for claw402 users (every 10 cycles) + if at.callCount%10 == 0 && store.IsClaw402Config(at.config.AIModel) { + at.checkClaw402Balance() + } + // Create decision record record := &store.DecisionRecord{ ExecutionLog: []string{}, @@ -110,6 +116,13 @@ func (at *AutoTrader) runCycle() error { } } + // Record AI charge (track cost regardless of decision outcome) + if aiDecision != nil && at.store != nil { + if chargeErr := at.store.AICharge().Record(at.id, at.aiModel, at.config.AIModel); chargeErr != nil { + logger.Warnf("⚠️ Failed to record AI charge: %v", chargeErr) + } + } + if err != nil { record.Success = false record.ErrorMessage = fmt.Sprintf("Failed to get AI decision: %v", err) @@ -558,3 +571,36 @@ func sortDecisionsByPriority(decisions []kernel.Decision) []kernel.Decision { return sorted } + +// checkClaw402Balance checks USDC balance and logs warnings if low +func (at *AutoTrader) checkClaw402Balance() { + scanMinutes := int(at.config.ScanInterval.Minutes()) + if scanMinutes <= 0 { + scanMinutes = 3 + } + dailyCost, _ := store.EstimateRunway(1.0, at.config.CustomModelName, scanMinutes) + logger.Infof("💰 [%s] Estimated daily AI cost: ~$%.2f (model: %s, interval: %dm)", + at.name, dailyCost, at.config.CustomModelName, scanMinutes) + + if at.claw402WalletAddr != "" { + balance, err := wallet.QueryUSDCBalance(at.claw402WalletAddr) + if err != nil { + logger.Warnf("⚠️ [%s] Failed to query USDC balance: %v", at.name, err) + return + } + + if balance < 1.0 { + logger.Warnf("⚠️ [%s] Low USDC balance: $%.2f — AI may stop soon!", at.name, balance) + } + if balance <= 0 { + logger.Errorf("🚨 [%s] USDC balance is ZERO — AI calls will fail!", at.name) + } + + runway := float64(0) + if dailyCost > 0 { + runway = balance / dailyCost + } + logger.Infof("💰 [%s] USDC Balance: $%.2f | Daily AI cost: ~$%.2f | Runway: ~%.1f days", + at.name, balance, dailyCost, runway) + } +} diff --git a/wallet/usdc.go b/wallet/usdc.go new file mode 100644 index 00000000..41952724 --- /dev/null +++ b/wallet/usdc.go @@ -0,0 +1,91 @@ +// Package wallet provides shared wallet utilities (USDC balance queries, etc.) +package wallet + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "math/big" + "net/http" + "strings" + "time" +) + +const ( + BaseRPCURL = "https://mainnet.base.org" + USDCContractBase = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" + USDCDecimals = 6 +) + +// QueryUSDCBalance queries USDC balance on Base chain and returns as float64 +func QueryUSDCBalance(address string) (float64, error) { + balanceStr := QueryUSDCBalanceStr(address) + var balance float64 + _, err := fmt.Sscanf(balanceStr, "%f", &balance) + if err != nil { + return 0, fmt.Errorf("failed to parse balance: %w", err) + } + return balance, nil +} + +// QueryUSDCBalanceStr queries USDC balance on Base chain and returns as formatted string +func QueryUSDCBalanceStr(address string) string { + // Build balanceOf(address) call data + // Function selector: 0x70a08231 + addrNoPre := strings.TrimPrefix(strings.ToLower(address), "0x") + data := "0x70a08231" + fmt.Sprintf("%064s", addrNoPre) + + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "method": "eth_call", + "params": []interface{}{ + map[string]string{ + "to": USDCContractBase, + "data": data, + }, + "latest", + }, + "id": 1, + } + + body, err := json.Marshal(payload) + if err != nil { + return "0.00" + } + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Post(BaseRPCURL, "application/json", bytes.NewReader(body)) + if err != nil { + return "0.00" + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "0.00" + } + + var rpcResp struct { + Result string `json:"result"` + } + if err := json.Unmarshal(respBody, &rpcResp); err != nil { + return "0.00" + } + + // Parse hex result + hexStr := strings.TrimPrefix(rpcResp.Result, "0x") + if hexStr == "" || hexStr == "0" { + return "0.00" + } + + balance := new(big.Int) + balance.SetString(hexStr, 16) + + // Convert to float with 6 decimals + divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(USDCDecimals), nil) + whole := new(big.Int).Div(balance, divisor) + remainder := new(big.Int).Mod(balance, divisor) + + return fmt.Sprintf("%d.%06d", whole, remainder) +} diff --git a/web/package-lock.json b/web/package-lock.json index e23f5c5e..bd3d7f5e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -18,6 +18,7 @@ "katex": "^0.16.27", "lightweight-charts": "^5.1.0", "lucide-react": "^0.552.0", + "qrcode.react": "^4.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-password-checklist": "^1.8.1", @@ -6837,6 +6838,15 @@ "node": ">=6" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/web/package.json b/web/package.json index 8bb7f7e6..beb2d07f 100644 --- a/web/package.json +++ b/web/package.json @@ -24,6 +24,7 @@ "katex": "^0.16.27", "lightweight-charts": "^5.1.0", "lucide-react": "^0.552.0", + "qrcode.react": "^4.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-password-checklist": "^1.8.1", diff --git a/web/src/components/trader/ModelConfigModal.tsx b/web/src/components/trader/ModelConfigModal.tsx index f58d89b4..dd1b1a11 100644 --- a/web/src/components/trader/ModelConfigModal.tsx +++ b/web/src/components/trader/ModelConfigModal.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react' +import { QRCodeSVG } from 'qrcode.react' import { Trash2, Brain, ExternalLink } from 'lucide-react' import type { AIModel } from '../../types' import type { Language } from '../../i18n/translations' @@ -298,6 +299,10 @@ function Claw402ConfigForm({ language: Language }) { const [walletAddress, setWalletAddress] = useState('') + const [copiedAddr, setCopiedAddr] = useState(false) + const [showDeposit, setShowDeposit] = useState(false) + const [showNewWalletBackup, setShowNewWalletBackup] = useState(false) + const [newWalletKey, setNewWalletKey] = useState('') const [usdcBalance, setUsdcBalance] = useState(null) const [keyError, setKeyError] = useState('') const [validating, setValidating] = useState(false) @@ -317,7 +322,7 @@ function Claw402ConfigForm({ const isKeyValid = apiKey.length === 66 && apiKey.startsWith('0x') && /^0x[0-9a-fA-F]{64}$/.test(apiKey) // Truncate address for display - const truncAddr = (addr: string) => addr ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : '' + // Debounced validation when apiKey changes useEffect(() => { @@ -375,6 +380,7 @@ function Claw402ConfigForm({ setWalletAddress(data.address || '') setUsdcBalance(data.balance_usdc || '0.00') setClaw402Status(data.claw402_status || 'unknown') + if (parseFloat(data.balance_usdc || '0') === 0) setShowDeposit(true) setTestResult({ status: data.claw402_status === 'ok' ? 'ok' : 'error', message: data.claw402_status === 'ok' @@ -446,6 +452,9 @@ function Claw402ConfigForm({
{m.provider} · {m.desc}
+
+ ~${m.price}/call +
{isSelected && ( @@ -485,19 +494,78 @@ function Claw402ConfigForm({
{t('modelConfig.walletPrivateKey', language)}
- onApiKeyChange(e.target.value)} - placeholder="0x..." - className="w-full px-4 py-3 rounded-xl font-mono text-sm" - style={{ - background: '#0B0E11', - border: keyError ? '1px solid #EF4444' : walletAddress ? '1px solid #00E096' : '1px solid #2B3139', - color: '#EAECEF', - }} - required - /> +
+ onApiKeyChange(e.target.value)} + placeholder="0x..." + className="flex-1 px-4 py-3 rounded-xl font-mono text-sm" + style={{ + background: '#0B0E11', + border: keyError ? '1px solid #EF4444' : walletAddress ? '1px solid #00E096' : '1px solid #2B3139', + color: '#EAECEF', + }} + required + /> + {!apiKey && ( + + )} +
+ + {/* New wallet backup warning */} + {showNewWalletBackup && newWalletKey && ( +
+
+ 🚨 {language === 'zh' ? '重要:请立即备份私钥!' : 'Important: Backup your private key NOW!'} +
+
+ {language === 'zh' + ? '这是你的钱包私钥,丢失后无法恢复,钱包里的资产将永久丢失。请复制并安全保存。' + : 'This is your wallet private key. If lost, it cannot be recovered and all assets will be permanently lost. Copy and save it securely.'} +
+
+ + {newWalletKey} + + +
+
+
✅ {language === 'zh' ? '建议保存到密码管理器(1Password / Bitwarden)' : 'Save to a password manager (1Password / Bitwarden)'}
+
✅ {language === 'zh' ? '或抄在纸上放安全的地方' : 'Or write it down and store it safely'}
+
❌ {language === 'zh' ? '不要截图发给别人' : 'Do NOT screenshot or share with anyone'}
+
+
+ )} +
🔒 @@ -528,20 +596,81 @@ function Claw402ConfigForm({ {/* Success: address + balance + status */} {walletAddress && !validating && !keyError && ( <> -
- - {t('modelConfig.walletAddress', language)}: {truncAddr(walletAddress)} +
+
+ + {t('modelConfig.walletAddress', language)}: + + +
+ {walletAddress} +
+ ⚠️ {language === 'zh' ? '请确认这是你的钱包地址(可在 MetaMask 中核对)' : 'Please confirm this is your wallet address (verify in MetaMask)'} +
{usdcBalance !== null && ( -
0 ? '#00E096' : '#F59E0B' }}> +
💰 - {t('modelConfig.usdcBalance', language)}: ${usdcBalance} + 0 ? '#00E096' : '#F59E0B' }}> + {t('modelConfig.usdcBalance', language)}: ${usdcBalance} + +
)} - {balanceNum === 0 && usdcBalance !== null && ( -
- 👉 - {t('modelConfig.depositUsdc', language)} + {showDeposit && ( +
+
+ 💳 {language === 'zh' ? '充值 USDC (Base 链)' : 'Deposit USDC (Base Chain)'} +
+
+
+ +
+
+
+ {language === 'zh' ? '扫码或复制地址转账' : 'Scan QR or copy address to transfer'} +
+ {walletAddress} + +
+
+
+
📱 {language === 'zh' ? '用交易所 App 扫描二维码直接转账' : 'Scan QR with exchange app to transfer'}
+
• {language === 'zh' ? '提币时网络选择 Base' : 'Choose Base network when withdrawing'}
+
• {language === 'zh' ? '或跨链桥: ' : 'Or bridge: '}bridge.base.org
+
• {language === 'zh' ? '最低充值 $1 USDC 即可开始' : 'Min $1 USDC to start'}
+
)} {claw402Status && ( diff --git a/web/src/components/trader/model-constants.ts b/web/src/components/trader/model-constants.ts index 340bfdf5..cfca87ab 100644 --- a/web/src/components/trader/model-constants.ts +++ b/web/src/components/trader/model-constants.ts @@ -12,6 +12,7 @@ export interface Claw402Model { provider: string desc: string icon: string + price: number // USD per call } export interface AIProviderConfig { @@ -52,18 +53,20 @@ export const BLOCKRUN_MODELS: BlockrunModel[] = [ // Models available through Claw402 (x402 USDC payment protocol) export const CLAW402_MODELS: Claw402Model[] = [ - { id: 'gpt-5.4', name: 'GPT-5.4', provider: 'OpenAI', desc: 'Flagship · Fast', icon: '⚡' }, - { id: 'gpt-5.4-pro', name: 'GPT-5.4 Pro', provider: 'OpenAI', desc: 'Reasoning · Pro', icon: '🧠' }, - { id: 'gpt-5.3', name: 'GPT-5.3', provider: 'OpenAI', desc: 'Balanced', icon: '💡' }, - { id: 'gpt-5-mini', name: 'GPT-5 Mini', provider: 'OpenAI', desc: 'Fast · Cheap', icon: '🚀' }, - { id: 'claude-opus', name: 'Claude Opus', provider: 'Anthropic', desc: 'Flagship · Deep', icon: '🎯' }, - { id: 'deepseek', name: 'DeepSeek V3', provider: 'DeepSeek', desc: 'Best Value', icon: '🔥' }, - { id: 'deepseek-reasoner', name: 'DeepSeek R1', provider: 'DeepSeek', desc: 'Reasoning', icon: '🤔' }, - { id: 'qwen-max', name: 'Qwen Max', provider: 'Alibaba', desc: 'Flagship', icon: '🌟' }, - { id: 'qwen-plus', name: 'Qwen Plus', provider: 'Alibaba', desc: 'Balanced', icon: '✨' }, - { id: 'grok-4.1', name: 'Grok 4.1', provider: 'xAI', desc: 'Flagship', icon: '⚡' }, - { id: 'gemini-3.1-pro', name: 'Gemini 3.1 Pro', provider: 'Google', desc: 'Flagship', icon: '💎' }, - { id: 'kimi-k2.5', name: 'Kimi K2.5', provider: 'Moonshot', desc: 'Balanced', icon: '🌙' }, + { id: 'deepseek', name: 'DeepSeek V3', provider: 'DeepSeek', desc: '$0.003/call', icon: '🔥', price: 0.003 }, + { id: 'deepseek-reasoner', name: 'DeepSeek R1', provider: 'DeepSeek', desc: '$0.005/call', icon: '🤔', price: 0.005 }, + { id: 'gpt-5-mini', name: 'GPT-5 Mini', provider: 'OpenAI', desc: '$0.005/call', icon: '🚀', price: 0.005 }, + { id: 'qwen-turbo', name: 'Qwen Turbo', provider: 'Alibaba', desc: '$0.002/call', icon: '⚡', price: 0.002 }, + { id: 'qwen-flash', name: 'Qwen Flash', provider: 'Alibaba', desc: '$0.002/call', icon: '⚡', price: 0.002 }, + { id: 'qwen-plus', name: 'Qwen Plus', provider: 'Alibaba', desc: '$0.005/call', icon: '✨', price: 0.005 }, + { id: 'kimi-k2.5', name: 'Kimi K2.5', provider: 'Moonshot', desc: '$0.008/call', icon: '🌙', price: 0.008 }, + { id: 'gpt-5.3', name: 'GPT-5.3', provider: 'OpenAI', desc: '$0.01/call', icon: '💡', price: 0.01 }, + { id: 'qwen-max', name: 'Qwen Max', provider: 'Alibaba', desc: '$0.01/call', icon: '🌟', price: 0.01 }, + { id: 'gemini-3.1-pro', name: 'Gemini 3.1 Pro', provider: 'Google', desc: '$0.03/call', icon: '💎', price: 0.03 }, + { id: 'gpt-5.4', name: 'GPT-5.4', provider: 'OpenAI', desc: '$0.05/call', icon: '⚡', price: 0.05 }, + { id: 'grok-4.1', name: 'Grok 4.1', provider: 'xAI', desc: '$0.06/call', icon: '⚡', price: 0.06 }, + { id: 'claude-opus', name: 'Claude Opus', provider: 'Anthropic', desc: '$0.12/call', icon: '🎯', price: 0.12 }, + { id: 'gpt-5.4-pro', name: 'GPT-5.4 Pro', provider: 'OpenAI', desc: '$0.50/call', icon: '🧠', price: 0.50 }, ] // AI Provider configuration - default models and API links