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
+46
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)
}
}