diff --git a/kernel/engine.go b/kernel/engine.go index 1fe9c958..3fd95106 100644 --- a/kernel/engine.go +++ b/kernel/engine.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "os" "nofx/logger" "nofx/market" "nofx/provider/hyperliquid" @@ -194,6 +195,21 @@ func NewStrategyEngine(config *store.StrategyConfig) *StrategyEngine { } client := nofxos.NewClient(nofxos.DefaultBaseURL, apiKey) + // If claw402 wallet key is available, route nofxos requests through claw402 + if walletKey := os.Getenv("CLAW402_WALLET_KEY"); walletKey != "" { + claw402URL := os.Getenv("CLAW402_URL") + if claw402URL == "" { + claw402URL = "https://claw402.ai" + } + claw402Client, err := nofxos.NewClaw402DataClient(claw402URL, walletKey, nil) + if err == nil { + client.SetClaw402(claw402Client) + logger.Infof("🔗 NofxOS data routed through claw402 (%s)", claw402URL) + } else { + logger.Warnf("⚠️ Failed to init claw402 data client: %v (using direct nofxos.ai)", err) + } + } + return &StrategyEngine{ config: config, nofxosClient: client, diff --git a/mcp/payment/x402.go b/mcp/payment/x402.go index 064cc086..7ce70fdc 100644 --- a/mcp/payment/x402.go +++ b/mcp/payment/x402.go @@ -81,6 +81,13 @@ func X402DecodeHeader(b64 string) ([]byte, error) { return decoded, nil } +// MakeClaw402SignFunc creates an X402SignFunc from a private key for claw402 payments. +func MakeClaw402SignFunc(privateKey *ecdsa.PrivateKey) X402SignFunc { + return func(paymentHeaderB64 string) (string, error) { + return SignBasePaymentHeader(privateKey, paymentHeaderB64, "Claw402") + } +} + // SignBasePaymentHeader decodes a base64 x402 header, parses it, and signs with // EIP-712 (USDC TransferWithAuthorization). func SignBasePaymentHeader(privateKey *ecdsa.PrivateKey, paymentHeaderB64 string, providerName string) (string, error) { diff --git a/provider/nofxos/claw402.go b/provider/nofxos/claw402.go new file mode 100644 index 00000000..0e4d6107 --- /dev/null +++ b/provider/nofxos/claw402.go @@ -0,0 +1,112 @@ +package nofxos + +import ( + "context" + "crypto/ecdsa" + "fmt" + "net/http" + "nofx/mcp" + "nofx/mcp/payment" + "os" + "strings" + "time" + + "github.com/ethereum/go-ethereum/crypto" +) + +// Claw402DataClient wraps nofxos API calls through claw402's x402 payment gateway. +// Instead of calling nofxos.ai directly, it calls claw402.ai/api/v1/nofx/... +// and pays with USDC for each request. +type Claw402DataClient struct { + claw402URL string + privateKey *ecdsa.PrivateKey + httpClient *http.Client + logger mcp.Logger +} + +// NewClaw402DataClient creates a client that routes nofxos requests through claw402. +// privateKeyHex is the wallet private key (0x-prefixed hex string). +func NewClaw402DataClient(claw402URL, privateKeyHex string, logger mcp.Logger) (*Claw402DataClient, error) { + if claw402URL == "" { + claw402URL = "https://claw402.ai" + } + claw402URL = strings.TrimRight(claw402URL, "/") + + if privateKeyHex == "" { + privateKeyHex = os.Getenv("CLAW402_WALLET_KEY") + } + if privateKeyHex == "" { + return nil, fmt.Errorf("claw402 wallet private key not set") + } + + hexKey := strings.TrimPrefix(privateKeyHex, "0x") + pk, err := crypto.HexToECDSA(hexKey) + if err != nil { + return nil, fmt.Errorf("invalid claw402 private key: %w", err) + } + + return &Claw402DataClient{ + claw402URL: claw402URL, + privateKey: pk, + httpClient: &http.Client{Timeout: 30 * time.Second}, + logger: logger, + }, nil +} + +// endpoint mapping: nofxos path → claw402 path +var endpointMap = map[string]string{ + "/api/ai500/list": "/api/v1/nofx/ai500/list", + "/api/ai500/stats": "/api/v1/nofx/ai500/stats", +} + +// mapEndpoint converts a nofxos endpoint to a claw402 endpoint. +// For endpoints not in the static map, applies the general pattern: +// /api/xxx → /api/v1/nofx/xxx +func mapEndpoint(nofxosPath string) string { + if mapped, ok := endpointMap[nofxosPath]; ok { + return mapped + } + // General pattern: /api/xxx → /api/v1/nofx/xxx + if strings.HasPrefix(nofxosPath, "/api/") { + return "/api/v1/nofx/" + strings.TrimPrefix(nofxosPath, "/api/") + } + return nofxosPath +} + +// DoRequest makes a GET request through claw402 with x402 payment. +func (c *Claw402DataClient) DoRequest(endpoint string) ([]byte, error) { + claw402Path := mapEndpoint(endpoint) + // Strip auth= query params (claw402 uses x402 payment, not auth keys) + if idx := strings.Index(claw402Path, "?auth="); idx != -1 { + claw402Path = claw402Path[:idx] + } + if idx := strings.Index(claw402Path, "&auth="); idx != -1 { + claw402Path = claw402Path[:idx] + } + + fullURL := c.claw402URL + claw402Path + + buildReq := func() (*http.Request, error) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fullURL, nil) + if err != nil { + return nil, err + } + req.Header.Set("X-Client-ID", "nofx") + return req, nil + } + + signFn := payment.MakeClaw402SignFunc(c.privateKey) + + body, err := payment.DoX402Request( + c.httpClient, + buildReq, + signFn, + "claw402-data", + c.logger, + ) + if err != nil { + return nil, fmt.Errorf("claw402 data request failed (%s): %w", claw402Path, err) + } + + return body, nil +} diff --git a/provider/nofxos/client.go b/provider/nofxos/client.go index a3a99a00..bee2dad5 100644 --- a/provider/nofxos/client.go +++ b/provider/nofxos/client.go @@ -25,6 +25,7 @@ type Client struct { AuthKey string Timeout time.Duration mu sync.RWMutex + claw402 *Claw402DataClient // If set, routes requests through claw402 } var ( @@ -59,6 +60,13 @@ func NewClient(baseURL, authKey string) *Client { } } +// SetClaw402 enables routing requests through claw402 payment gateway. +func (c *Client) SetClaw402(claw402Client *Claw402DataClient) { + c.mu.Lock() + defer c.mu.Unlock() + c.claw402 = claw402Client +} + // SetConfig updates client configuration func (c *Client) SetConfig(baseURL, authKey string) { c.mu.Lock() @@ -85,14 +93,21 @@ func (c *Client) GetAuthKey() string { return c.AuthKey } -// doRequest performs an HTTP GET request with authentication +// doRequest performs an HTTP GET request with authentication. +// If claw402 client is configured, routes through claw402 payment gateway instead. func (c *Client) doRequest(endpoint string) ([]byte, error) { c.mu.RLock() + claw402Client := c.claw402 baseURL := c.BaseURL authKey := c.AuthKey timeout := c.Timeout c.mu.RUnlock() + // Route through claw402 if configured + if claw402Client != nil { + return claw402Client.DoRequest(endpoint) + } + url := baseURL + endpoint if !strings.Contains(url, "auth=") { if strings.Contains(url, "?") {