mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 01:48:22 +08:00
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:
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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, "?") {
|
||||
|
||||
Reference in New Issue
Block a user