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:
shinchan-zhai
2026-03-21 12:31:20 +08:00
parent 79a513470b
commit fd77f2df3e
12 changed files with 629 additions and 98 deletions

View File

@@ -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()
}

View File

@@ -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)
}
}