feat: add Claw402 (claw402.ai) x402 USDC payment provider

Add Claw402Client for claw402.ai's x402 micropayment gateway (Base USDC).
Supports 15+ AI models (GPT-5.4, Claude Opus, DeepSeek, Qwen, Grok, etc.)
with per-model endpoint routing.

- mcp/claw402.go: new client with model→endpoint mapping, x402 v2 payment flow
- mcp/blockrun_base.go: extract shared signX402Payment() for reuse
- Register "claw402" provider in all 6 consumer switch statements:
  api/server.go, api/strategy.go, trader/auto_trader.go,
  telegram/bot.go, debate/engine.go, backtest/ai_client.go
This commit is contained in:
tinkle-community
2026-03-10 17:53:13 +08:00
parent af250825e7
commit 156bf04bcc
8 changed files with 283 additions and 35 deletions

View File

@@ -3450,6 +3450,7 @@ func (s *Server) handleGetSupportedModels(c *gin.Context) {
{"id": "minimax", "name": "MiniMax", "provider": "minimax", "defaultModel": "MiniMax-M2.5"},
{"id": "blockrun-base", "name": "BlockRun (Base Wallet)", "provider": "blockrun-base", "defaultModel": "auto"},
{"id": "blockrun-sol", "name": "BlockRun (Solana Wallet)", "provider": "blockrun-sol", "defaultModel": "auto"},
{"id": "claw402", "name": "Claw402 (Base USDC)", "provider": "claw402", "defaultModel": "deepseek"},
}
c.JSON(http.StatusOK, supportedModels)

View File

@@ -674,6 +674,9 @@ func (s *Server) runRealAITest(userID, modelID, systemPrompt, userPrompt string)
case "blockrun-sol":
aiClient = mcp.NewBlockRunSolClient()
aiClient.SetAPIKey(apiKey, "", model.CustomModelName)
case "claw402":
aiClient = mcp.NewClaw402Client()
aiClient.SetAPIKey(apiKey, "", model.CustomModelName)
default:
// Use generic client
aiClient = mcp.NewClient()

View File

@@ -92,6 +92,13 @@ func configureMCPClient(cfg BacktestConfig, base mcp.AIClient) (mcp.AIClient, er
brSol := mcp.NewBlockRunSolClient()
brSol.SetAPIKey(cfg.AICfg.APIKey, "", cfg.AICfg.Model)
return brSol, nil
case "claw402":
if cfg.AICfg.APIKey == "" {
return nil, fmt.Errorf("claw402 provider requires wallet private key")
}
claw := mcp.NewClaw402Client()
claw.SetAPIKey(cfg.AICfg.APIKey, "", cfg.AICfg.Model)
return claw, nil
case "custom":
if cfg.AICfg.BaseURL == "" || cfg.AICfg.APIKey == "" || cfg.AICfg.Model == "" {
return nil, fmt.Errorf("custom provider requires base_url, api key and model")

View File

@@ -103,6 +103,8 @@ func (e *DebateEngine) InitializeClients(participants []*store.DebateParticipant
client = mcp.NewBlockRunBaseClient()
case "blockrun-sol":
client = mcp.NewBlockRunSolClient()
case "claw402":
client = mcp.NewClaw402Client()
default:
client = mcp.New()
}

View File

@@ -169,21 +169,9 @@ func (c *BlockRunBaseClient) call(systemPrompt, userPrompt string) (string, erro
// x402v2PaymentRequired is the structure of the X-Payment-Required header (x402 v2).
type x402v2PaymentRequired struct {
X402Version int `json:"x402Version"`
Accepts []struct {
Scheme string `json:"scheme"`
Network string `json:"network"`
Amount string `json:"amount"`
Asset string `json:"asset"`
PayTo string `json:"payTo"`
MaxTimeoutSeconds int `json:"maxTimeoutSeconds"`
Extra map[string]string `json:"extra"`
} `json:"accepts"`
Resource *struct {
URL string `json:"url"`
Description string `json:"description"`
MimeType string `json:"mimeType"`
} `json:"resource"`
X402Version int `json:"x402Version"`
Accepts []x402AcceptOption `json:"accepts"`
Resource *x402Resource `json:"resource"`
}
// signPayment parses the X-Payment-Required header (x402 v2) and returns a signed X-Payment value.
@@ -192,7 +180,6 @@ func (c *BlockRunBaseClient) signPayment(paymentHeaderB64 string) (string, error
return "", fmt.Errorf("no private key set for BlockRun Base wallet")
}
// Decode base64 → JSON
decoded, err := base64.RawStdEncoding.DecodeString(paymentHeaderB64)
if err != nil {
decoded, err = base64.StdEncoding.DecodeString(paymentHeaderB64)
@@ -205,12 +192,34 @@ func (c *BlockRunBaseClient) signPayment(paymentHeaderB64 string) (string, error
if err := json.Unmarshal(decoded, &req); err != nil {
return "", fmt.Errorf("failed to parse x402 v2 payment header: %w", err)
}
if len(req.Accepts) == 0 {
return "", fmt.Errorf("no payment options in x402 response")
}
opt := req.Accepts[0]
senderAddr := crypto.PubkeyToAddress(c.privateKey.PublicKey).Hex()
return signX402Payment(c.privateKey, senderAddr, req.Accepts[0], req.Resource)
}
// x402AcceptOption is the payment option from x402 v2 header (extracted for shared signing).
type x402AcceptOption struct {
Scheme string `json:"scheme"`
Network string `json:"network"`
Amount string `json:"amount"`
Asset string `json:"asset"`
PayTo string `json:"payTo"`
MaxTimeoutSeconds int `json:"maxTimeoutSeconds"`
Extra map[string]string `json:"extra"`
}
type x402Resource struct {
URL string `json:"url"`
Description string `json:"description"`
MimeType string `json:"mimeType"`
}
// signX402Payment is the shared EIP-712 signing logic for x402 v2 on Base USDC.
// Used by both BlockRunBaseClient and Claw402Client.
func signX402Payment(privateKey *ecdsa.PrivateKey, senderAddr string, opt x402AcceptOption, resource *x402Resource) (string, error) {
recipient := opt.PayTo
amount := opt.Amount
network := opt.Network
@@ -224,28 +233,22 @@ func (c *BlockRunBaseClient) signPayment(paymentHeaderB64 string) (string, error
resourceURL := ""
resourceDesc := ""
resourceMime := "application/json"
if req.Resource != nil {
resourceURL = req.Resource.URL
resourceDesc = req.Resource.Description
resourceMime = req.Resource.MimeType
if resource != nil {
resourceURL = resource.URL
resourceDesc = resource.Description
resourceMime = resource.MimeType
}
// Timestamps: validAfter = now-600 (clock skew), validBefore = now+maxTimeout
now := time.Now().Unix()
validAfter := now - 600
validBefore := now + int64(maxTimeout)
// Random nonce (bytes32)
nonceBytes := make([]byte, 32)
if _, err := rand.Read(nonceBytes); err != nil {
return "", fmt.Errorf("failed to generate nonce: %w", err)
}
nonce := "0x" + hex.EncodeToString(nonceBytes)
// Sender address
senderAddr := crypto.PubkeyToAddress(c.privateKey.PublicKey).Hex()
// Build EIP-712 domain separator
domainName := "USD Coin"
domainVersion := "2"
if extra != nil {
@@ -262,7 +265,6 @@ func (c *BlockRunBaseClient) signPayment(paymentHeaderB64 string) (string, error
return "", fmt.Errorf("failed to build domain separator: %w", err)
}
// Build struct hash
amountBig, err := parseBigInt(amount)
if err != nil {
return "", fmt.Errorf("invalid amount: %w", err)
@@ -273,26 +275,22 @@ func (c *BlockRunBaseClient) signPayment(paymentHeaderB64 string) (string, error
return "", fmt.Errorf("failed to build struct hash: %w", err)
}
// EIP-712 digest
digest := make([]byte, 0, 66)
digest = append(digest, 0x19, 0x01)
digest = append(digest, domainSeparator...)
digest = append(digest, structHash...)
hash := keccak256Bytes(digest)
// Sign with secp256k1
sig, err := crypto.Sign(hash, c.privateKey)
sig, err := crypto.Sign(hash, privateKey)
if err != nil {
return "", fmt.Errorf("failed to sign: %w", err)
}
// Adjust V: go-ethereum returns 0/1, EIP-712 expects 27/28
if sig[64] < 27 {
sig[64] += 27
}
sigHex := "0x" + hex.EncodeToString(sig)
// Build x402 v2 payment payload
paymentData := map[string]interface{}{
"x402Version": 2,
"resource": map[string]string{

230
mcp/claw402.go Normal file
View File

@@ -0,0 +1,230 @@
package mcp
import (
"bytes"
"crypto/ecdsa"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/ethereum/go-ethereum/crypto"
)
const (
ProviderClaw402 = "claw402"
DefaultClaw402URL = "https://claw402.ai"
DefaultClaw402Model = "deepseek"
)
// claw402ModelEndpoints maps user-friendly model names to claw402 API paths.
var claw402ModelEndpoints = map[string]string{
// OpenAI
"gpt-5.4": "/api/v1/ai/openai/chat/5.4",
"gpt-5.4-pro": "/api/v1/ai/openai/chat/5.4-pro",
"gpt-5.3": "/api/v1/ai/openai/chat/5.3",
"gpt-5-mini": "/api/v1/ai/openai/chat/5-mini",
// Anthropic
"claude-opus": "/api/v1/ai/anthropic/messages/opus",
// DeepSeek
"deepseek": "/api/v1/ai/deepseek/chat",
"deepseek-reasoner": "/api/v1/ai/deepseek/chat/reasoner",
// Qwen
"qwen-max": "/api/v1/ai/qwen/chat/max",
"qwen-plus": "/api/v1/ai/qwen/chat/plus",
"qwen-turbo": "/api/v1/ai/qwen/chat/turbo",
"qwen-flash": "/api/v1/ai/qwen/chat/flash",
// Grok
"grok-4.1": "/api/v1/ai/grok/chat/4.1",
// Gemini
"gemini-3.1-pro": "/api/v1/ai/gemini/chat/3.1-pro",
// Kimi
"kimi-k2.5": "/api/v1/ai/kimi/chat/k2.5",
}
// Claw402Client implements AIClient using claw402.ai's x402 v2 USDC payment gateway.
// Reuses the same EIP-712 signing as BlockRunBaseClient (same Base chain + USDC contract).
type Claw402Client struct {
*Client
privateKey *ecdsa.PrivateKey
}
// NewClaw402Client creates a claw402 client (backward compatible).
func NewClaw402Client() AIClient {
return NewClaw402ClientWithOptions()
}
// NewClaw402ClientWithOptions creates a claw402 client with options.
func NewClaw402ClientWithOptions(opts ...ClientOption) AIClient {
baseOpts := []ClientOption{
WithProvider(ProviderClaw402),
WithModel(DefaultClaw402Model),
WithBaseURL(DefaultClaw402URL),
}
allOpts := append(baseOpts, opts...)
baseClient := NewClient(allOpts...).(*Client)
baseClient.UseFullURL = true
baseClient.BaseURL = DefaultClaw402URL + claw402ModelEndpoints[DefaultClaw402Model]
c := &Claw402Client{Client: baseClient}
baseClient.hooks = c
return c
}
// SetAPIKey stores the EVM private key and selects the model endpoint.
func (c *Claw402Client) SetAPIKey(apiKey string, _ string, customModel string) {
hexKey := strings.TrimPrefix(apiKey, "0x")
privKey, err := crypto.HexToECDSA(hexKey)
if err != nil {
c.logger.Warnf("⚠️ [MCP] Claw402: invalid private key: %v", err)
} else {
c.privateKey = privKey
c.APIKey = apiKey
addr := crypto.PubkeyToAddress(privKey.PublicKey).Hex()
c.logger.Infof("🔧 [MCP] Claw402 wallet: %s", addr)
}
if customModel != "" {
c.Model = customModel
}
endpoint := c.resolveEndpoint()
c.BaseURL = DefaultClaw402URL + endpoint
c.logger.Infof("🔧 [MCP] Claw402 model: %s → %s", c.Model, endpoint)
}
// resolveEndpoint returns the API path for the configured model.
func (c *Claw402Client) resolveEndpoint() string {
if ep, ok := claw402ModelEndpoints[c.Model]; ok {
return ep
}
// Allow raw path override (e.g. "/api/v1/ai/openai/chat/5.4")
if strings.HasPrefix(c.Model, "/api/") {
return c.Model
}
return claw402ModelEndpoints[DefaultClaw402Model]
}
func (c *Claw402Client) setAuthHeader(_ http.Header) {
// No Bearer token — payment is via x402 signing
}
// call handles the x402 v2 payment flow for claw402.ai.
func (c *Claw402Client) call(systemPrompt, userPrompt string) (string, error) {
c.logger.Infof("📡 [Claw402] Request AI: %s", c.BaseURL)
requestBody := c.hooks.buildMCPRequestBody(systemPrompt, userPrompt)
jsonData, err := c.hooks.marshalRequestBody(requestBody)
if err != nil {
return "", err
}
url := c.hooks.buildUrl()
req, err := c.hooks.buildRequest(url, jsonData)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// Handle x402 Payment Required
if resp.StatusCode == http.StatusPaymentRequired {
// Check both header variants
paymentHeader := resp.Header.Get("X-Payment-Required")
if paymentHeader == "" {
paymentHeader = resp.Header.Get("Payment-Required")
}
if paymentHeader == "" {
// Try reading payment details from response body
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("received 402 but no payment header found. Body: %s", string(body))
}
paymentSig, err := c.signPayment(paymentHeader)
if err != nil {
return "", fmt.Errorf("failed to sign x402 payment: %w", err)
}
req2, err := c.hooks.buildRequest(url, jsonData)
if err != nil {
return "", fmt.Errorf("failed to build retry request: %w", err)
}
// Send payment in both header variants for compatibility
req2.Header.Set("X-Payment", paymentSig)
req2.Header.Set("Payment-Signature", paymentSig)
resp2, err := c.httpClient.Do(req2)
if err != nil {
return "", fmt.Errorf("failed to send payment retry: %w", err)
}
defer resp2.Body.Close()
body2, err := io.ReadAll(resp2.Body)
if err != nil {
return "", fmt.Errorf("failed to read payment response: %w", err)
}
if resp2.StatusCode != http.StatusOK {
return "", fmt.Errorf("Claw402 payment retry failed (status %d): %s", resp2.StatusCode, string(body2))
}
return c.hooks.parseMCPResponse(body2)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("Claw402 API error (status %d): %s", resp.StatusCode, string(body))
}
return c.hooks.parseMCPResponse(body)
}
// signPayment reuses the same EIP-712 signing logic as BlockRunBaseClient
// (same Base chain, same USDC contract, same TransferWithAuthorization).
func (c *Claw402Client) signPayment(paymentHeaderB64 string) (string, error) {
if c.privateKey == nil {
return "", fmt.Errorf("no private key set for Claw402 wallet")
}
// Decode base64 → JSON
decoded, err := base64.RawStdEncoding.DecodeString(paymentHeaderB64)
if err != nil {
decoded, err = base64.StdEncoding.DecodeString(paymentHeaderB64)
if err != nil {
return "", fmt.Errorf("failed to decode payment header: %w", err)
}
}
var req x402v2PaymentRequired
if err := json.Unmarshal(decoded, &req); err != nil {
return "", fmt.Errorf("failed to parse x402 v2 payment header: %w", err)
}
if len(req.Accepts) == 0 {
return "", fmt.Errorf("no payment options in x402 response")
}
// Reuse the same signing logic as BlockRunBaseClient — identical chain + USDC contract
opt := req.Accepts[0]
senderAddr := crypto.PubkeyToAddress(c.privateKey.PublicKey).Hex()
return signX402Payment(c.privateKey, senderAddr, opt, req.Resource)
}
// buildUrl returns the full claw402 endpoint URL.
func (c *Claw402Client) buildUrl() string {
return c.BaseURL
}
// buildRequest creates the HTTP request without Authorization header.
func (c *Claw402Client) buildRequest(url string, jsonData []byte) (*http.Request, error) {
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("fail to build request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
return req, nil
}

View File

@@ -315,7 +315,7 @@ func newLLMClient(st *store.Store, userID string) mcp.AIClient {
// isUSDCProvider returns true for providers that pay per call with USDC (x402 protocol).
func isUSDCProvider(provider string) bool {
return provider == "blockrun-base" || provider == "blockrun-sol"
return provider == "blockrun-base" || provider == "blockrun-sol" || provider == "claw402"
}
func clientForProvider(provider string) mcp.AIClient {
@@ -340,6 +340,8 @@ func clientForProvider(provider string) mcp.AIClient {
return mcp.NewBlockRunBaseClient()
case "blockrun-sol":
return mcp.NewBlockRunSolClient()
case "claw402":
return mcp.NewClaw402Client()
default:
return mcp.NewDeepSeekClient()
}

View File

@@ -216,6 +216,11 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au
mcpClient.SetAPIKey(config.CustomAPIKey, "", config.CustomModelName)
logger.Infof("🤖 [%s] Using BlockRun (Solana Wallet) AI", config.Name)
case "claw402":
mcpClient = mcp.NewClaw402Client()
mcpClient.SetAPIKey(config.CustomAPIKey, "", config.CustomModelName)
logger.Infof("🤖 [%s] Using Claw402 (Base USDC) AI", config.Name)
case "qwen":
mcpClient = mcp.NewQwenClient()
apiKey := config.QwenKey