feat: server-side launch preflight with structured checks and runtime AI wallet health

- POST /api/launch/preflight and GET /api/traders/:id/preflight run every
  launch prerequisite (AI model credential, claw402 wallet key + Base USDC
  balance, strategy, exchange config, live account state, funding minimums)
  and return a structured checklist with stable codes; minimums (1 USDC AI
  fee / 12 USDC trading) now live server-side and are exposed in the response.
- POST /traders/:id/start enforces the preflight (400 with error_key
  trader.start.preflight_failed + full checklist; ?force=true to override).
  Funding gates use max(available, equity) so restarting a bot with deployed
  margin is not blocked; indeterminate probes (RPC outage) degrade to
  warnings instead of blocking.
- Run loop now surfaces runtime health via GetStatus: safe_mode(+reason) and
  ai_wallet_status/balance (ok|low|empty|unknown), including typed detection
  of claw402 ErrInsufficientFunds instead of burying it in logs; safe-mode
  state is mutex-guarded for API readers.
- Onboarding wallet endpoints switch to the cached, error-aware balance query
  and report balance_status so the UI can tell an RPC outage from an empty
  wallet.
This commit is contained in:
tinkle-community
2026-07-05 20:38:38 +09:00
parent e61f12d307
commit b17d97effc
10 changed files with 843 additions and 14 deletions

View File

@@ -26,6 +26,7 @@ type beginnerOnboardingResponse struct {
DefaultModel string `json:"default_model"`
ConfiguredModelID string `json:"configured_model_id"`
BalanceUSDC string `json:"balance_usdc"`
BalanceStatus string `json:"balance_status,omitempty"`
EnvSaved bool `json:"env_saved"`
EnvPath string `json:"env_path,omitempty"`
ReusedExisting bool `json:"reused_existing"`
@@ -36,10 +37,22 @@ type currentBeginnerWalletResponse struct {
Found bool `json:"found"`
Address string `json:"address,omitempty"`
BalanceUSDC string `json:"balance_usdc,omitempty"`
BalanceStatus string `json:"balance_status,omitempty"`
Source string `json:"source,omitempty"`
Claw402Status string `json:"claw402_status"`
}
// queryBeginnerWalletBalance returns the wallet balance plus a status flag so
// the UI can distinguish "RPC unreachable" from a genuinely empty wallet.
// Uses the 30s cache — safe for UI polling.
func queryBeginnerWalletBalance(address string) (balanceUSDC string, balanceStatus string) {
balance, err := wallet.QueryUSDCBalanceCached(address)
if err != nil {
return "", "unknown"
}
return fmt.Sprintf("%.2f", balance), "ok"
}
func (s *Server) handleBeginnerOnboarding(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
@@ -72,6 +85,7 @@ func (s *Server) handleBeginnerOnboarding(c *gin.Context) {
os.Setenv("CLAW402_DEFAULT_MODEL", payment.DefaultClaw402Model)
envSaved, envPath, envErr := persistBeginnerWalletEnv(privateKey, address)
balanceUSDC, balanceStatus := queryBeginnerWalletBalance(address)
resp := beginnerOnboardingResponse{
Address: address,
PrivateKey: privateKey,
@@ -80,7 +94,8 @@ func (s *Server) handleBeginnerOnboarding(c *gin.Context) {
Provider: "claw402",
DefaultModel: payment.DefaultClaw402Model,
ConfiguredModelID: configuredModelID,
BalanceUSDC: wallet.QueryUSDCBalanceStr(address),
BalanceUSDC: balanceUSDC,
BalanceStatus: balanceStatus,
EnvSaved: envSaved,
EnvPath: envPath,
ReusedExisting: reusedExisting,
@@ -124,10 +139,12 @@ func (s *Server) handleCurrentBeginnerWallet(c *gin.Context) {
continue
}
balanceUSDC, balanceStatus := queryBeginnerWalletBalance(address)
c.JSON(http.StatusOK, currentBeginnerWalletResponse{
Found: true,
Address: address,
BalanceUSDC: wallet.QueryUSDCBalanceStr(address),
BalanceUSDC: balanceUSDC,
BalanceStatus: balanceStatus,
Source: "model",
Claw402Status: claw402Status,
})
@@ -136,10 +153,12 @@ func (s *Server) handleCurrentBeginnerWallet(c *gin.Context) {
address := strings.TrimSpace(os.Getenv("CLAW402_WALLET_ADDRESS"))
if address != "" {
balanceUSDC, balanceStatus := queryBeginnerWalletBalance(address)
c.JSON(http.StatusOK, currentBeginnerWalletResponse{
Found: true,
Address: address,
BalanceUSDC: wallet.QueryUSDCBalanceStr(address),
BalanceUSDC: balanceUSDC,
BalanceStatus: balanceStatus,
Source: "env",
Claw402Status: claw402Status,
})