Files
nofx/provider/nofxos/claw402.go
T
shinchan-zhai 2d68b48f52 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).
2026-03-25 09:58:24 +08:00

113 lines
3.0 KiB
Go

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
}