feat: route nofxos data API calls through claw402 x402 payment

When CLAW402_WALLET_KEY env var is set, all nofxos.ai data API calls
(AI500, OI rankings, NetFlow, price rankings) are automatically routed
through claw402.ai with x402 USDC micropayment.

- provider/nofxos/claw402.go: x402 GET request client for data APIs
- provider/nofxos/client.go: claw402 mode support in doRequest()
- kernel/engine.go: auto-detect CLAW402_WALLET_KEY and enable routing
- mcp/payment/x402.go: MakeClaw402SignFunc helper

Without CLAW402_WALLET_KEY, falls back to direct nofxos.ai (backward compat).
This commit is contained in:
shinchan-zhai
2026-03-25 09:58:24 +08:00
parent 9b14c5c84d
commit 2d68b48f52
4 changed files with 151 additions and 1 deletions
+16
View File
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os"
"nofx/logger" "nofx/logger"
"nofx/market" "nofx/market"
"nofx/provider/hyperliquid" "nofx/provider/hyperliquid"
@@ -194,6 +195,21 @@ func NewStrategyEngine(config *store.StrategyConfig) *StrategyEngine {
} }
client := nofxos.NewClient(nofxos.DefaultBaseURL, apiKey) 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{ return &StrategyEngine{
config: config, config: config,
nofxosClient: client, nofxosClient: client,
+7
View File
@@ -81,6 +81,13 @@ func X402DecodeHeader(b64 string) ([]byte, error) {
return decoded, nil 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 // SignBasePaymentHeader decodes a base64 x402 header, parses it, and signs with
// EIP-712 (USDC TransferWithAuthorization). // EIP-712 (USDC TransferWithAuthorization).
func SignBasePaymentHeader(privateKey *ecdsa.PrivateKey, paymentHeaderB64 string, providerName string) (string, error) { func SignBasePaymentHeader(privateKey *ecdsa.PrivateKey, paymentHeaderB64 string, providerName string) (string, error) {
+112
View File
@@ -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
}
+16 -1
View File
@@ -25,6 +25,7 @@ type Client struct {
AuthKey string AuthKey string
Timeout time.Duration Timeout time.Duration
mu sync.RWMutex mu sync.RWMutex
claw402 *Claw402DataClient // If set, routes requests through claw402
} }
var ( 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 // SetConfig updates client configuration
func (c *Client) SetConfig(baseURL, authKey string) { func (c *Client) SetConfig(baseURL, authKey string) {
c.mu.Lock() c.mu.Lock()
@@ -85,14 +93,21 @@ func (c *Client) GetAuthKey() string {
return c.AuthKey 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) { func (c *Client) doRequest(endpoint string) ([]byte, error) {
c.mu.RLock() c.mu.RLock()
claw402Client := c.claw402
baseURL := c.BaseURL baseURL := c.BaseURL
authKey := c.AuthKey authKey := c.AuthKey
timeout := c.Timeout timeout := c.Timeout
c.mu.RUnlock() c.mu.RUnlock()
// Route through claw402 if configured
if claw402Client != nil {
return claw402Client.DoRequest(endpoint)
}
url := baseURL + endpoint url := baseURL + endpoint
if !strings.Contains(url, "auth=") { if !strings.Contains(url, "auth=") {
if strings.Contains(url, "?") { if strings.Contains(url, "?") {