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"
|
"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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
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, "?") {
|
||||||
|
|||||||
Reference in New Issue
Block a user