mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 15:26:55 +08:00
feat: AI cost tracking, pre-launch balance check, low balance alerts
- store/ai_charge.go: local AI cost tracking per call (SQLite) - wallet/usdc.go: shared USDC balance query (Base chain RPC) - Pre-launch: estimate daily cost + runway days - Low balance: warn <$1, error at $0 (every 10 cycles) - API: GET /api/ai-costs for cost history - Frontend: model cards show price per call - Frontend: wallet create + QR deposit + balance display
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user