mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +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:
43
api/handler_ai_cost.go
Normal file
43
api/handler_ai_cost.go
Normal file
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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":<bool>}`,
|
||||
`: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":"<EXACT id — use this as ai_model_id when creating/updating a trader>","name":"<display name>","provider":"<short provider name — NOT a valid id>","enabled":<bool>}]
|
||||
|
||||
Reference in New Issue
Block a user