From 966995fb888e0094a0986ab8e1eaabf77505bf7b Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Tue, 24 Mar 2026 01:44:54 +0800 Subject: [PATCH] refactor: remove BlockRun provider, retain Claw402 as sole x402 payment provider Remove all BlockRun (Base + Solana wallet) references from codebase: - Delete blockrun_base.go, blockrun_sol.go, wallet setup docs, icon - Move shared EIP-712 signing code to x402.go for Claw402 reuse - Clean up provider constants, model lists, UI components, translations - Update all README files (EN + 6 i18n) and getting-started docs --- README.md | 13 +- api/handler_ai_model.go | 2 - api/strategy.go | 2 +- docs/getting-started/README.md | 13 - docs/getting-started/blockrun-base-wallet.md | 126 ------- docs/getting-started/blockrun-sol-wallet.md | 120 ------ docs/i18n/ja/README.md | 7 +- docs/i18n/ko/README.md | 7 +- docs/i18n/ru/README.md | 7 +- docs/i18n/uk/README.md | 7 +- docs/i18n/vi/README.md | 7 +- docs/i18n/zh-CN/README.md | 7 +- mcp/client.go | 6 +- mcp/payment/blockrun_base.go | 352 ------------------ mcp/payment/blockrun_sol.go | 276 -------------- mcp/payment/claw402.go | 2 +- mcp/payment/x402.go | 256 ++++++++++++- mcp/providers.go | 4 +- telegram/bot.go | 2 +- telemetry/experience.go | 2 +- trader/auto_trader.go | 4 +- web/public/icons/blockrun.svg | 6 - web/src/components/common/ModelIcons.tsx | 6 - .../components/trader/ModelConfigModal.tsx | 154 +++----- web/src/components/trader/model-constants.ts | 26 -- web/src/i18n/translations.ts | 3 - 26 files changed, 316 insertions(+), 1101 deletions(-) delete mode 100644 docs/getting-started/blockrun-base-wallet.md delete mode 100644 docs/getting-started/blockrun-sol-wallet.md delete mode 100644 mcp/payment/blockrun_base.go delete mode 100644 mcp/payment/blockrun_sol.go delete mode 100644 web/public/icons/blockrun.svg diff --git a/README.md b/README.md index 369f695b..534593b1 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ React x402 Claw402 - BlockRun

@@ -63,10 +62,6 @@ No accounts. No API keys. No prepaid credits. One wallet, every model. | Provider | Chain | Models | |:---------|:------|:-------| | **[Claw402](https://claw402.ai)** | Base | GPT-5.4, Claude Opus, DeepSeek, Qwen, Grok, Gemini, Kimi — 15+ models | -| **[BlockRun](https://blockrun.ai)** | Base | Configurable | -| **[BlockRun Sol](https://sol.blockrun.ai)** | Solana | Configurable | - -Also compatible with **[ClawRouter](https://github.com/BlockRunAI/ClawRouter)** — an intelligent LLM router that picks the cheapest capable model per request (41+ models, 74-100% savings, <1ms routing). --- @@ -118,7 +113,7 @@ Crypto · US Stocks · Forex · Metals ### AI Models (x402 Mode — No API Key) -15+ models via [Claw402](https://claw402.ai) or [BlockRun](https://blockrun.ai) — just a USDC wallet +15+ models via [Claw402](https://claw402.ai) — just a USDC wallet --- @@ -257,9 +252,9 @@ curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bas ├──────────┴──────────┴──────────┴────────────────┤ │ MCP AI Client Layer │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ - │ │ API Key │ │ x402 │ │ ClawRouter│ │ - │ │ DeepSeek │ │ Claw402 │ │ 41+ models│ │ - │ │ GPT,Claude │ │ BlockRun │ │ auto-route│ │ + │ │ API Key │ │ x402 │ │ │ │ + │ │ DeepSeek │ │ Claw402 │ │ │ │ + │ │ GPT,Claude │ │ │ │ │ │ │ └───────────┘ └───────────┘ └───────────┘ │ ├─────────────────────────────────────────────────┤ │ Exchange Connectors │ diff --git a/api/handler_ai_model.go b/api/handler_ai_model.go index d30d5dec..32badbd0 100644 --- a/api/handler_ai_model.go +++ b/api/handler_ai_model.go @@ -202,8 +202,6 @@ func (s *Server) handleGetSupportedModels(c *gin.Context) { {"id": "grok", "name": "Grok (xAI)", "provider": "grok", "defaultModel": "grok-3-latest"}, {"id": "kimi", "name": "Kimi (Moonshot)", "provider": "kimi", "defaultModel": "moonshot-v1-auto"}, {"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"}, } diff --git a/api/strategy.go b/api/strategy.go index 2fc9c9fb..c58f3278 100644 --- a/api/strategy.go +++ b/api/strategy.go @@ -650,7 +650,7 @@ func (s *Server) runRealAITest(userID, modelID, systemPrompt, userPrompt string) // Payment providers ignore custom URL switch provider { - case "blockrun-base", "blockrun-sol", "claw402": + case "claw402": aiClient.SetAPIKey(apiKey, "", model.CustomModelName) default: aiClient.SetAPIKey(apiKey, model.CustomAPIURL, model.CustomModelName) diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index a447028b..1f79a75f 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -44,19 +44,6 @@ Use custom AI models or third-party OpenAI-compatible APIs: --- -### 💳 BlockRun Wallet (Pay-per-Request, No API Key) - -Access all top AI models by paying with USDC — no API key signup required. - -| Provider | Guide | Payment Network | -|----------|-------|-----------------| -| BlockRun (Base Wallet) | [blockrun-base-wallet.md](blockrun-base-wallet.md) | Base (EVM) · USDC | -| BlockRun (Solana Wallet) | [blockrun-sol-wallet.md](blockrun-sol-wallet.md) | Solana · USDC | - -**How it works:** Each AI request automatically pays a micro-USDC fee via the [x402 payment protocol](https://blockrun.ai). Your private key signs the payment authorization — no funds leave your wallet until the AI response is delivered. - ---- - ## 🔑 Prerequisites Before starting, ensure you have: diff --git a/docs/getting-started/blockrun-base-wallet.md b/docs/getting-started/blockrun-base-wallet.md deleted file mode 100644 index 7487ccea..00000000 --- a/docs/getting-started/blockrun-base-wallet.md +++ /dev/null @@ -1,126 +0,0 @@ -# BlockRun Base (EVM) Wallet Setup Guide - -This guide explains how to use a Base network EVM wallet to pay for AI usage through BlockRun — no API key required. - -**Language:** [English](blockrun-base-wallet.md) | [中文](blockrun-base-wallet.zh-CN.md) - -## What is BlockRun? - -[BlockRun](https://blockrun.ai) is a decentralized AI inference gateway that lets you access top AI models (Claude, GPT, Gemini, Grok, DeepSeek, etc.) by paying per request with USDC — no monthly subscriptions, no API key signups. - -NOFX integrates BlockRun via the **x402 micropayment protocol**: each AI inference request automatically pays a small USDC fee directly from your wallet. You only pay for what you use. - -## Why Use BlockRun? - -| Feature | Traditional API Key | BlockRun Wallet | -|---------|-------------------|-----------------| -| Setup | Register + billing | Just a wallet address | -| Cost model | Monthly subscription | Pay-per-request | -| Models | One provider | All top models | -| Privacy | Account required | Pseudonymous | -| Control | Rate limits apply | Your wallet, your budget | - -## Prerequisites - -- An EVM wallet with USDC on **Base network** (chain ID 8453) -- The wallet private key (hex format: `0x...`) - -### Getting USDC on Base - -1. Buy USDC on Coinbase and withdraw to Base, **or** -2. Bridge USDC from Ethereum using [bridge.base.org](https://bridge.base.org), **or** -3. Swap on [Aerodrome](https://aerodrome.finance) or [Uniswap](https://app.uniswap.org) on Base - -> **Tip:** A few dollars of USDC is enough to start — each AI call costs fractions of a cent. - -## Step 1: Get Your Wallet Private Key - -> ⚠️ **Security Warning:** Never share your private key with anyone. Use a dedicated trading wallet, not your main holdings wallet. - -**Option A — Create a new wallet (recommended):** -1. Open MetaMask → Create New Account -2. Go to Account Details → Export Private Key -3. Copy the hex key (starts with `0x`) - -**Option B — Use an existing wallet:** -1. MetaMask → Account Details → Export Private Key -2. Enter your MetaMask password to reveal the key - -**Option C — Generate via CLI:** -```bash -# Using cast (foundry) -cast wallet new -# Output: Address: 0x... | Private key: 0x... -``` - -## Step 2: Fund the Wallet with USDC on Base - -Send USDC to your wallet address on Base network: -- **USDC contract:** `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` -- **Network:** Base (chain ID 8453) -- **Recommended starting amount:** $5–$20 USDC - -Check your balance at [basescan.org](https://basescan.org). - -## Step 3: Configure in NOFX - -1. Open NOFX at `http://localhost:3000` -2. Log in and go to **Config** tab -3. Click **+ Add AI Model** -4. In Step 0, scroll to **Via BlockRun Wallet** section -5. Select **BlockRun · Base Wallet** -6. In Step 1, configure: - - **Wallet Private Key:** Your hex private key (`0x...`) - - **Select Model:** Choose from Claude Opus, GPT-5.4, Gemini 3 Pro, Grok 3, DeepSeek R1, or leave as **Auto** for best available -7. Click **Save** - -## How Payment Works - -When NOFX sends an AI request: - -1. Request goes to `https://blockrun.ai/api/v1/chat/completions` -2. Server responds with HTTP `402 Payment Required` + payment details -3. NOFX signs a **ERC-3009 TransferWithAuthorization** (EIP-712) with your private key -4. Payment signature is attached and request is retried -5. BlockRun verifies the signature, routes the request to the AI model, and charges USDC - -> **Privacy:** Your private key never leaves your NOFX instance. Only the cryptographic signature is sent. - -## Available Models via BlockRun - -| Model ID | Provider | Use Case | -|----------|----------|----------| -| `gpt-5.4` | OpenAI | Flagship (default) | -| `claude-opus-4.6` | Anthropic | Flagship | -| `gemini-3.1-pro` | Google | Flagship | -| `grok-3` | xAI | Flagship | -| `deepseek-chat` | DeepSeek | Flagship | -| `minimax-m2.5` | MiniMax | Flagship | - -## Security Best Practices - -- ✅ Use a **dedicated wallet** with only trading budget, not your main wallet -- ✅ Keep only a small USDC balance (top up as needed) -- ✅ Your private key is encrypted at rest in NOFX's database -- ✅ Signatures are spend-limited — each signature authorizes only the exact amount for one request -- ❌ Never export or share your private key outside of NOFX - -## Troubleshooting - -| Issue | Solution | -|-------|----------| -| `no private key set` | Check your key was saved correctly; re-enter in Config | -| `payment retry failed` | Ensure you have USDC on **Base** (not Ethereum mainnet) | -| `invalid private key` | Key must be hex format with `0x` prefix, 66 chars total | -| Payment deducted but no response | Check BlockRun status at [blockrun.ai](https://blockrun.ai) | -| Slow responses | Try selecting a specific model instead of "Auto" | - -## Monitoring Spend - -Check your USDC balance and transaction history at: -- [Basescan](https://basescan.org) — search your wallet address -- [BlockRun dashboard](https://blockrun.ai) — usage history - ---- - -[← Back to Getting Started](README.md) diff --git a/docs/getting-started/blockrun-sol-wallet.md b/docs/getting-started/blockrun-sol-wallet.md deleted file mode 100644 index 3e7b12b0..00000000 --- a/docs/getting-started/blockrun-sol-wallet.md +++ /dev/null @@ -1,120 +0,0 @@ -# BlockRun Solana Wallet Setup Guide - -This guide explains how to use a Solana wallet to pay for AI usage through BlockRun — no API key required. - -**Language:** [English](blockrun-sol-wallet.md) | [中文](blockrun-sol-wallet.zh-CN.md) - -## What is BlockRun? - -[BlockRun](https://blockrun.ai) is a decentralized AI inference gateway that lets you access top AI models (Claude, GPT, Gemini, Grok, DeepSeek, etc.) by paying per request with USDC — no monthly subscriptions, no API key signups. - -NOFX integrates BlockRun via the **x402 micropayment protocol** on Solana: each AI inference request automatically pays a small USDC fee directly from your wallet. - -## Prerequisites - -- A Solana wallet with USDC on **Solana mainnet** -- The wallet private key (base58-encoded, 64 bytes — standard Solana keypair format) - -### Getting USDC on Solana - -1. Buy SOL on any exchange and withdraw to your Solana wallet, then swap to USDC on [Jupiter](https://jup.ag), **or** -2. Buy USDC directly on an exchange and withdraw to Solana, **or** -3. Bridge from other chains using [Wormhole](https://wormhole.com) - -> **Tip:** A few dollars of USDC is plenty to start. - -## Step 1: Export Your Solana Private Key - -> ⚠️ **Security Warning:** Use a dedicated wallet for NOFX — not your main holdings wallet. - -**From Phantom Wallet:** -1. Open Phantom → Settings (gear icon) -2. Security & Privacy → Export Private Key -3. Enter your password -4. Copy the base58 key (looks like: `5J...` — a long string of ~88 characters) - -**From Solflare:** -1. Settings → Export Private Key -2. The key is displayed in base58 format - -**From CLI (solana-keygen):** -```bash -# View existing keypair -cat ~/.config/solana/id.json -# This is a JSON array — convert to base58 using: -solana-keygen pubkey ~/.config/solana/id.json -``` - -> **Note:** NOFX accepts the **base58-encoded 64-byte keypair** (as exported by Phantom/Solflare). This is the standard format for Solana private keys. - -## Step 2: Fund the Wallet with USDC on Solana - -Send USDC to your Solana wallet: -- **USDC SPL token mint:** `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` -- **Network:** Solana Mainnet -- **Recommended starting amount:** $5–$20 USDC - -Check your balance at [solscan.io](https://solscan.io) or in your wallet app. - -## Step 3: Configure in NOFX - -1. Open NOFX at `http://localhost:3000` -2. Log in and go to **Config** tab -3. Click **+ Add AI Model** -4. In Step 0, scroll to **Via BlockRun Wallet** section -5. Select **BlockRun · Solana Wallet** -6. In Step 1, configure: - - **Wallet Private Key:** Your base58-encoded Solana private key - - **Select Model:** Choose from Claude Opus, GPT-5.4, Gemini 3 Pro, Grok 3, DeepSeek R1, or leave as **Auto** for best available -7. Click **Save** - -## How Payment Works - -When NOFX sends an AI request: - -1. Request goes to `https://sol.blockrun.ai/api/v1/chat/completions` -2. Server responds with HTTP `402 Payment Required` + payment details (nonce, recipient, amount) -3. NOFX signs the payment message `blockrun-payment:{nonce}:{recipient}:{amount}` with your **Ed25519** private key -4. Payment signature is attached and request is retried -5. BlockRun verifies the Ed25519 signature on-chain and routes to the AI model - -> **Privacy:** Your private key never leaves your NOFX instance. Only the cryptographic signature is sent. - -## Available Models via BlockRun - -| Model ID | Provider | Use Case | -|----------|----------|----------| -| `gpt-5.4` | OpenAI | Flagship (default) | -| `claude-opus-4.6` | Anthropic | Flagship | -| `gemini-3.1-pro` | Google | Flagship | -| `grok-3` | xAI | Flagship | -| `deepseek-chat` | DeepSeek | Flagship | -| `minimax-m2.5` | MiniMax | Flagship | - -## Security Best Practices - -- ✅ Use a **dedicated trading wallet** with only your AI budget -- ✅ Keep only a small USDC balance (top up as needed) -- ✅ Your private key is AES-256 encrypted at rest in NOFX's database -- ✅ Ed25519 signatures are one-time — each authorizes only one specific payment -- ❌ Never use your main SOL holdings wallet as the NOFX trading wallet - -## Troubleshooting - -| Issue | Solution | -|-------|----------| -| `unexpected key length` | Ensure you exported the full 64-byte keypair (not just the 32-byte seed) | -| `failed to decode base58` | Key must be base58 encoded (standard Phantom/Solflare export format) | -| `payment retry failed` | Ensure you have USDC on **Solana mainnet** (not devnet) | -| No response from server | Check `sol.blockrun.ai` is reachable from your server | -| Slow responses | Try selecting a specific model instead of "Auto" | - -## Monitoring Spend - -Check your USDC balance and transaction history at: -- [Solscan](https://solscan.io) — search your wallet address, filter by USDC token -- [BlockRun dashboard](https://blockrun.ai) — usage history - ---- - -[← Back to Getting Started](README.md) diff --git a/docs/i18n/ja/README.md b/docs/i18n/ja/README.md index bd91e98d..ab02527a 100644 --- a/docs/i18n/ja/README.md +++ b/docs/i18n/ja/README.md @@ -17,7 +17,6 @@ React x402 Claw402 - BlockRun

@@ -63,10 +62,6 @@ x402 フロー: | プロバイダー | チェーン | モデル | |:---------|:------|:-------| | **[Claw402](https://claw402.ai)** | Base | GPT-5.4, Claude Opus, DeepSeek, Qwen, Grok, Gemini, Kimi — 15+ モデル | -| **[BlockRun](https://blockrun.ai)** | Base | 設定可能 | -| **[BlockRun Sol](https://sol.blockrun.ai)** | Solana | 設定可能 | - -**[ClawRouter](https://github.com/BlockRunAI/ClawRouter)** とも互換 — リクエストごとに最安のモデルを自動選択するインテリジェント LLM ルーター(41+ モデル、74-100% 節約、<1ms ルーティング)。 --- @@ -120,7 +115,7 @@ x402 フロー: ### AI モデル (x402 モード — API キー不要) -15+ モデルを [Claw402](https://claw402.ai) または [BlockRun](https://blockrun.ai) 経由で利用 — USDC ウォレットのみ +15+ モデルを [Claw402](https://claw402.ai) 経由で利用 — USDC ウォレットのみ --- diff --git a/docs/i18n/ko/README.md b/docs/i18n/ko/README.md index 7ac5ff6d..fd66d8e3 100644 --- a/docs/i18n/ko/README.md +++ b/docs/i18n/ko/README.md @@ -17,7 +17,6 @@ React x402 Claw402 - BlockRun

@@ -63,10 +62,6 @@ x402 플로우: | 프로바이더 | 체인 | 모델 | |:---------|:------|:-------| | **[Claw402](https://claw402.ai)** | Base | GPT-5.4, Claude Opus, DeepSeek, Qwen, Grok, Gemini, Kimi — 15+ 모델 | -| **[BlockRun](https://blockrun.ai)** | Base | 설정 가능 | -| **[BlockRun Sol](https://sol.blockrun.ai)** | Solana | 설정 가능 | - -**[ClawRouter](https://github.com/BlockRunAI/ClawRouter)** 호환 — 요청마다 최저가 모델을 자동 선택하는 지능형 LLM 라우터 (41+ 모델, 74-100% 절감, <1ms 라우팅). --- @@ -120,7 +115,7 @@ x402 플로우: ### AI 모델 (x402 모드 — API 키 불필요) -15+ 모델을 [Claw402](https://claw402.ai) 또는 [BlockRun](https://blockrun.ai)으로 이용 — USDC 지갑만 있으면 됩니다 +15+ 모델을 [Claw402](https://claw402.ai)로 이용 — USDC 지갑만 있으면 됩니다 --- diff --git a/docs/i18n/ru/README.md b/docs/i18n/ru/README.md index 9496034f..2dc6363e 100644 --- a/docs/i18n/ru/README.md +++ b/docs/i18n/ru/README.md @@ -17,7 +17,6 @@ React x402 Claw402 - BlockRun

@@ -63,10 +62,6 @@ x402 процесс: | Провайдер | Сеть | Модели | |:---------|:------|:-------| | **[Claw402](https://claw402.ai)** | Base | GPT-5.4, Claude Opus, DeepSeek, Qwen, Grok, Gemini, Kimi — 15+ моделей | -| **[BlockRun](https://blockrun.ai)** | Base | Настраиваемый | -| **[BlockRun Sol](https://sol.blockrun.ai)** | Solana | Настраиваемый | - -Совместим с **[ClawRouter](https://github.com/BlockRunAI/ClawRouter)** — интеллектуальный LLM маршрутизатор, автоматически выбирающий самую дешёвую модель (41+ моделей, экономия 74-100%, <1ms маршрутизация). --- @@ -120,7 +115,7 @@ x402 процесс: ### AI Модели (Режим x402 — без API ключей) -15+ моделей через [Claw402](https://claw402.ai) или [BlockRun](https://blockrun.ai) — только USDC кошелёк +15+ моделей через [Claw402](https://claw402.ai) — только USDC кошелёк --- diff --git a/docs/i18n/uk/README.md b/docs/i18n/uk/README.md index b7a9e21f..1bf1ef7a 100644 --- a/docs/i18n/uk/README.md +++ b/docs/i18n/uk/README.md @@ -17,7 +17,6 @@ React x402 Claw402 - BlockRun

@@ -63,10 +62,6 @@ x402 процес: | Провайдер | Мережа | Моделі | |:---------|:------|:-------| | **[Claw402](https://claw402.ai)** | Base | GPT-5.4, Claude Opus, DeepSeek, Qwen, Grok, Gemini, Kimi — 15+ моделей | -| **[BlockRun](https://blockrun.ai)** | Base | Налаштовуваний | -| **[BlockRun Sol](https://sol.blockrun.ai)** | Solana | Налаштовуваний | - -Сумісний з **[ClawRouter](https://github.com/BlockRunAI/ClawRouter)** — інтелектуальний LLM маршрутизатор (41+ моделей, економія 74-100%, <1ms маршрутизація). --- @@ -120,7 +115,7 @@ x402 процес: ### AI Моделі (Режим x402 — без API ключів) -15+ моделей через [Claw402](https://claw402.ai) або [BlockRun](https://blockrun.ai) — лише USDC гаманець +15+ моделей через [Claw402](https://claw402.ai) — лише USDC гаманець --- diff --git a/docs/i18n/vi/README.md b/docs/i18n/vi/README.md index e89805ef..cd507a08 100644 --- a/docs/i18n/vi/README.md +++ b/docs/i18n/vi/README.md @@ -17,7 +17,6 @@ React x402 Claw402 - BlockRun

@@ -63,10 +62,6 @@ Không tài khoản. Không API key. Không trả trước. Một ví, tất c | Nhà cung cấp | Chain | Mô hình | |:---------|:------|:-------| | **[Claw402](https://claw402.ai)** | Base | GPT-5.4, Claude Opus, DeepSeek, Qwen, Grok, Gemini, Kimi — 15+ mô hình | -| **[BlockRun](https://blockrun.ai)** | Base | Có thể cấu hình | -| **[BlockRun Sol](https://sol.blockrun.ai)** | Solana | Có thể cấu hình | - -Tương thích với **[ClawRouter](https://github.com/BlockRunAI/ClawRouter)** — bộ định tuyến LLM thông minh tự động chọn mô hình rẻ nhất (41+ mô hình, tiết kiệm 74-100%, <1ms định tuyến). --- @@ -118,7 +113,7 @@ Crypto · Cổ phiếu Mỹ · Forex · Kim loại ### Mô hình AI (Chế độ x402 — Không cần API Key) -15+ mô hình qua [Claw402](https://claw402.ai) hoặc [BlockRun](https://blockrun.ai) — chỉ cần ví USDC +15+ mô hình qua [Claw402](https://claw402.ai) — chỉ cần ví USDC --- diff --git a/docs/i18n/zh-CN/README.md b/docs/i18n/zh-CN/README.md index 28bc8565..e24b99ea 100644 --- a/docs/i18n/zh-CN/README.md +++ b/docs/i18n/zh-CN/README.md @@ -17,7 +17,6 @@ React x402 Claw402 - BlockRun

@@ -65,10 +64,6 @@ x402 流程: | 提供商 | 链 | 模型 | |:---------|:------|:-------| | **[Claw402](https://claw402.ai)** | Base | GPT-5.4、Claude Opus、DeepSeek、Qwen、Grok、Gemini、Kimi — 15+ 模型 | -| **[BlockRun](https://blockrun.ai)** | Base | 可配置 | -| **[BlockRun Sol](https://sol.blockrun.ai)** | Solana | 可配置 | - -同时兼容 **[ClawRouter](https://github.com/BlockRunAI/ClawRouter)** —— 智能 LLM 路由,自动选择每次请求最便宜的模型(41+ 模型,节省 74-100%,<1ms 路由)。 --- @@ -121,7 +116,7 @@ x402 流程: ### AI 模型 (x402 模式 — 无需 API Key) -15+ 模型通过 [Claw402](https://claw402.ai) 或 [BlockRun](https://blockrun.ai) 接入 — 只需一个 USDC 钱包 +15+ 模型通过 [Claw402](https://claw402.ai) 接入 — 只需一个 USDC 钱包 --- diff --git a/mcp/client.go b/mcp/client.go index 2d5c864c..99b0fec2 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -44,7 +44,7 @@ var ( // TokenUsage represents token usage from AI API response type TokenUsage struct { - Provider string // payment channel: "claw402", "blockrun-base", "blockrun-sol", or native provider name + Provider string // payment channel: "claw402" or native provider name Model string PromptTokens int CompletionTokens int @@ -52,13 +52,11 @@ type TokenUsage struct { } // Channel returns the payment channel category for telemetry. -// Returns "claw402", "blockrun", or "native" based on the provider. +// Returns "claw402" or "native" based on the provider. func (u TokenUsage) Channel() string { switch u.Provider { case ProviderClaw402: return "claw402" - case ProviderBlockRunBase, ProviderBlockRunSol: - return "blockrun" default: return "native" } diff --git a/mcp/payment/blockrun_base.go b/mcp/payment/blockrun_base.go deleted file mode 100644 index e7394f54..00000000 --- a/mcp/payment/blockrun_base.go +++ /dev/null @@ -1,352 +0,0 @@ -package payment - -import ( - "crypto/ecdsa" - "crypto/rand" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "math/big" - "net/http" - "strings" - "time" - - "github.com/ethereum/go-ethereum/crypto" - "golang.org/x/crypto/sha3" - - "nofx/mcp" -) - -const ( - DefaultBlockRunBaseURL = "https://blockrun.ai" - DefaultBlockRunModel = "gpt-5.4" - BlockRunChatEndpoint = "/api/v1/chat/completions" - BaseUSDCContract = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" - BaseChainID int64 = 8453 - BaseNetwork = "eip155:8453" -) - -// EIP-712 type hashes for USDC TransferWithAuthorization (ERC-3009) -var ( - eip712DomainTypeHash = keccak256String("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") - transferWithAuthTypeHash = keccak256String("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)") -) - -func init() { - mcp.RegisterProvider(mcp.ProviderBlockRunBase, func(opts ...mcp.ClientOption) mcp.AIClient { - return NewBlockRunBaseClientWithOptions(opts...) - }) -} - -func keccak256String(s string) []byte { - h := sha3.NewLegacyKeccak256() - h.Write([]byte(s)) - return h.Sum(nil) -} - -func keccak256Bytes(data ...[]byte) []byte { - h := sha3.NewLegacyKeccak256() - for _, b := range data { - h.Write(b) - } - return h.Sum(nil) -} - -// BlockRunBaseClient implements AIClient using BlockRun's API with x402 v2 EIP-712 payment signing. -type BlockRunBaseClient struct { - *mcp.Client - privateKey *ecdsa.PrivateKey -} - -func (c *BlockRunBaseClient) BaseClient() *mcp.Client { return c.Client } - -// NewBlockRunBaseClient creates a BlockRun Base wallet client (backward compatible). -func NewBlockRunBaseClient() mcp.AIClient { - return NewBlockRunBaseClientWithOptions() -} - -// NewBlockRunBaseClientWithOptions creates a BlockRun Base wallet client. -func NewBlockRunBaseClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient { - baseOpts := []mcp.ClientOption{ - mcp.WithProvider(mcp.ProviderBlockRunBase), - mcp.WithModel(DefaultBlockRunModel), - mcp.WithBaseURL(DefaultBlockRunBaseURL), - mcp.WithTimeout(X402Timeout), - mcp.WithMaxRetries(1), // disable outer retry — inner x402 loop handles retries; outer retry causes duplicate payments - } - allOpts := append(baseOpts, opts...) - baseClient := mcp.NewClient(allOpts...).(*mcp.Client) - baseClient.UseFullURL = true - baseClient.BaseURL = DefaultBlockRunBaseURL + BlockRunChatEndpoint - - c := &BlockRunBaseClient{Client: baseClient} - baseClient.Hooks = c - return c -} - -// SetAPIKey stores the EVM private key (hex, with or without 0x prefix). -func (c *BlockRunBaseClient) SetAPIKey(apiKey string, customURL string, customModel string) { - hexKey := strings.TrimPrefix(apiKey, "0x") - privKey, err := crypto.HexToECDSA(hexKey) - if err != nil { - c.Log.Warnf("⚠️ [MCP] BlockRun Base: invalid private key: %v", err) - } else { - c.privateKey = privKey - c.APIKey = apiKey - addr := crypto.PubkeyToAddress(privKey.PublicKey).Hex() - c.Log.Infof("🔧 [MCP] BlockRun Base wallet: %s", addr) - } - if customModel != "" { - c.Model = customModel - c.Log.Infof("🔧 [MCP] BlockRun Base model: %s", customModel) - } else { - c.Log.Infof("🔧 [MCP] BlockRun Base model: %s", DefaultBlockRunModel) - } -} - -func (c *BlockRunBaseClient) SetAuthHeader(h http.Header) { X402SetAuthHeader(h) } - -func (c *BlockRunBaseClient) Call(systemPrompt, userPrompt string) (string, error) { - return X402Call(c.Client, c.signPayment, "BlockRun Base", systemPrompt, userPrompt) -} - -func (c *BlockRunBaseClient) CallWithRequestFull(req *mcp.Request) (*mcp.LLMResponse, error) { - return X402CallFull(c.Client, c.signPayment, "BlockRun Base", req) -} - -// signPayment parses the Payment-Required header (x402 v2) and returns a signed payment value. -func (c *BlockRunBaseClient) signPayment(paymentHeaderB64 string) (string, error) { - return SignBasePaymentHeader(c.privateKey, paymentHeaderB64, "BlockRun Base") -} - -// 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 - asset := opt.Asset - extra := opt.Extra - maxTimeout := opt.MaxTimeoutSeconds - if maxTimeout == 0 { - maxTimeout = 300 - } - - resourceURL := "" - resourceDesc := "" - resourceMime := "application/json" - if resource != nil { - resourceURL = resource.URL - resourceDesc = resource.Description - resourceMime = resource.MimeType - } - - now := time.Now().Unix() - validAfter := int64(0) - validBefore := now + int64(maxTimeout) - - 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) - - domainName := "USD Coin" - domainVersion := "2" - if extra != nil { - if v, ok := extra["name"]; ok && v != "" { - domainName = v - } - if v, ok := extra["version"]; ok && v != "" { - domainVersion = v - } - } - - domainSeparator, err := buildDomainSeparatorDynamic(domainName, domainVersion, network, asset) - if err != nil { - return "", fmt.Errorf("failed to build domain separator: %w", err) - } - - amountBig, err := parseBigInt(amount) - if err != nil { - return "", fmt.Errorf("invalid amount: %w", err) - } - - structHash, err := buildTransferWithAuthHashDynamic(senderAddr, recipient, amountBig, validAfter, validBefore, nonce) - if err != nil { - return "", fmt.Errorf("failed to build struct hash: %w", err) - } - - digest := make([]byte, 0, 66) - digest = append(digest, 0x19, 0x01) - digest = append(digest, domainSeparator...) - digest = append(digest, structHash...) - hash := keccak256Bytes(digest) - - sig, err := crypto.Sign(hash, privateKey) - if err != nil { - return "", fmt.Errorf("failed to sign: %w", err) - } - if sig[64] < 27 { - sig[64] += 27 - } - - sigHex := "0x" + hex.EncodeToString(sig) - - paymentData := map[string]interface{}{ - "x402Version": 2, - "resource": map[string]string{ - "url": resourceURL, - "description": resourceDesc, - "mimeType": resourceMime, - }, - "accepted": map[string]interface{}{ - "scheme": "exact", - "network": network, - "amount": amount, - "asset": asset, - "payTo": recipient, - "maxTimeoutSeconds": maxTimeout, - "extra": extra, - }, - "payload": map[string]interface{}{ - "signature": sigHex, - "authorization": map[string]string{ - "from": senderAddr, - "to": recipient, - "value": amount, - "validAfter": fmt.Sprintf("%d", validAfter), - "validBefore": fmt.Sprintf("%d", validBefore), - "nonce": nonce, - }, - }, - "extensions": map[string]interface{}{}, - } - - resultJSON, err := json.Marshal(paymentData) - if err != nil { - return "", fmt.Errorf("failed to marshal payment result: %w", err) - } - - return base64.StdEncoding.EncodeToString(resultJSON), nil -} - -// buildDomainSeparatorDynamic builds the EIP-712 domain separator using runtime values. -func buildDomainSeparatorDynamic(name, version, network, asset string) ([]byte, error) { - chainID := new(big.Int).SetInt64(BaseChainID) - if strings.HasPrefix(network, "eip155:") { - parts := strings.SplitN(network, ":", 2) - if len(parts) == 2 { - if n, ok := new(big.Int).SetString(parts[1], 10); ok { - chainID = n - } - } - } - - contractAddr, err := hex.DecodeString(strings.TrimPrefix(asset, "0x")) - if err != nil { - return nil, fmt.Errorf("invalid contract address: %w", err) - } - - nameHash := keccak256String(name) - versionHash := keccak256String(version) - - encoded := make([]byte, 0, 5*32) - encoded = append(encoded, leftPad32(eip712DomainTypeHash)...) - encoded = append(encoded, leftPad32(nameHash)...) - encoded = append(encoded, leftPad32(versionHash)...) - encoded = append(encoded, leftPad32(chainID.Bytes())...) - addrPadded := make([]byte, 32) - copy(addrPadded[32-len(contractAddr):], contractAddr) - encoded = append(encoded, addrPadded...) - - return keccak256Bytes(encoded), nil -} - -// buildTransferWithAuthHashDynamic builds the struct hash for TransferWithAuthorization. -func buildTransferWithAuthHashDynamic(from, to string, value *big.Int, validAfter, validBefore int64, nonce string) ([]byte, error) { - fromBytes, err := hexToAddress(from) - if err != nil { - return nil, fmt.Errorf("invalid from address: %w", err) - } - toBytes, err := hexToAddress(to) - if err != nil { - return nil, fmt.Errorf("invalid to address: %w", err) - } - nonceBytes, err := hexToBytes32(nonce) - if err != nil { - return nil, fmt.Errorf("invalid nonce: %w", err) - } - - validAfterBig := new(big.Int).SetInt64(validAfter) - validBeforeBig := new(big.Int).SetInt64(validBefore) - - encoded := make([]byte, 0, 7*32) - encoded = append(encoded, leftPad32(transferWithAuthTypeHash)...) - encoded = append(encoded, leftPad32(fromBytes)...) - encoded = append(encoded, leftPad32(toBytes)...) - encoded = append(encoded, leftPad32(value.Bytes())...) - encoded = append(encoded, leftPad32(validAfterBig.Bytes())...) - encoded = append(encoded, leftPad32(validBeforeBig.Bytes())...) - encoded = append(encoded, leftPad32(nonceBytes)...) - - return keccak256Bytes(encoded), nil -} - -func hexToAddress(s string) ([]byte, error) { - s = strings.TrimPrefix(s, "0x") - b, err := hex.DecodeString(s) - if err != nil { - return nil, err - } - if len(b) != 20 { - return nil, fmt.Errorf("address must be 20 bytes, got %d", len(b)) - } - return b, nil -} - -func hexToBytes32(s string) ([]byte, error) { - s = strings.TrimPrefix(s, "0x") - b, err := hex.DecodeString(s) - if err != nil { - return nil, err - } - if len(b) > 32 { - return nil, fmt.Errorf("nonce too long: %d bytes", len(b)) - } - return b, nil -} - -func parseBigInt(s string) (*big.Int, error) { - n := new(big.Int) - if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { - if _, ok := n.SetString(s[2:], 16); ok { - return n, nil - } - return nil, fmt.Errorf("cannot parse hex big.Int from %q", s) - } - if _, ok := n.SetString(s, 10); ok { - return n, nil - } - return nil, fmt.Errorf("cannot parse big.Int from %q", s) -} - -// leftPad32 pads a byte slice to 32 bytes on the left (ABI encoding). -func leftPad32(b []byte) []byte { - if len(b) >= 32 { - return b[:32] - } - padded := make([]byte, 32) - copy(padded[32-len(b):], b) - return padded -} - -// BuildUrl returns the full BlockRun endpoint URL. -func (c *BlockRunBaseClient) BuildUrl() string { - return DefaultBlockRunBaseURL + BlockRunChatEndpoint -} - -func (c *BlockRunBaseClient) BuildRequest(url string, jsonData []byte) (*http.Request, error) { - return X402BuildRequest(url, jsonData) -} diff --git a/mcp/payment/blockrun_sol.go b/mcp/payment/blockrun_sol.go deleted file mode 100644 index 95a3e75f..00000000 --- a/mcp/payment/blockrun_sol.go +++ /dev/null @@ -1,276 +0,0 @@ -package payment - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/programs/compute-budget" - "github.com/gagliardetto/solana-go/programs/token" - "github.com/gagliardetto/solana-go/rpc" - - "nofx/mcp" -) - -const ( - DefaultBlockRunSolURL = "https://sol.blockrun.ai" - SolanaUSDCMint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" - SolanaNetwork = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" - SolanaMainnetRPC = "https://api.mainnet-beta.solana.com" - - // Compute budget defaults (match @x402/svm) - computeUnitLimit = uint32(8000) - computeUnitPrice = uint64(1) -) - -func init() { - mcp.RegisterProvider(mcp.ProviderBlockRunSol, func(opts ...mcp.ClientOption) mcp.AIClient { - return NewBlockRunSolClientWithOptions(opts...) - }) -} - -// BlockRunSolClient implements AIClient using BlockRun's Solana x402 v2 payment protocol. -type BlockRunSolClient struct { - *mcp.Client - keypair solana.PrivateKey -} - -func (c *BlockRunSolClient) BaseClient() *mcp.Client { return c.Client } - -// NewBlockRunSolClient creates a BlockRun Solana wallet client (backward compatible). -func NewBlockRunSolClient() mcp.AIClient { - return NewBlockRunSolClientWithOptions() -} - -// NewBlockRunSolClientWithOptions creates a BlockRun Solana wallet client. -func NewBlockRunSolClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient { - baseOpts := []mcp.ClientOption{ - mcp.WithProvider(mcp.ProviderBlockRunSol), - mcp.WithModel(DefaultBlockRunModel), - mcp.WithBaseURL(DefaultBlockRunSolURL), - mcp.WithTimeout(X402Timeout), - mcp.WithMaxRetries(1), // disable outer retry — inner x402 loop handles retries; outer retry causes duplicate payments - } - allOpts := append(baseOpts, opts...) - baseClient := mcp.NewClient(allOpts...).(*mcp.Client) - baseClient.UseFullURL = true - baseClient.BaseURL = DefaultBlockRunSolURL + BlockRunChatEndpoint - - c := &BlockRunSolClient{Client: baseClient} - baseClient.Hooks = c - return c -} - -// SetAPIKey stores the Solana wallet private key (base58-encoded 64-byte keypair). -func (c *BlockRunSolClient) SetAPIKey(apiKey string, customURL string, customModel string) { - kp, err := solana.PrivateKeyFromBase58(strings.TrimSpace(apiKey)) - if err != nil { - c.Log.Warnf("⚠️ [MCP] BlockRun Sol: failed to parse private key: %v", err) - return - } - c.keypair = kp - c.APIKey = apiKey - c.Log.Infof("🔧 [MCP] BlockRun Sol wallet: %s", kp.PublicKey().String()) - - if customModel != "" { - c.Model = customModel - c.Log.Infof("🔧 [MCP] BlockRun Sol model: %s", customModel) - } else { - c.Log.Infof("🔧 [MCP] BlockRun Sol model: %s", DefaultBlockRunModel) - } -} - -func (c *BlockRunSolClient) SetAuthHeader(h http.Header) { X402SetAuthHeader(h) } - -func (c *BlockRunSolClient) Call(systemPrompt, userPrompt string) (string, error) { - return X402Call(c.Client, c.signSolanaPayment, "BlockRun Sol", systemPrompt, userPrompt) -} - -func (c *BlockRunSolClient) CallWithRequestFull(req *mcp.Request) (*mcp.LLMResponse, error) { - return X402CallFull(c.Client, c.signSolanaPayment, "BlockRun Sol", req) -} - -// signSolanaPayment parses the Payment-Required header and builds a signed x402 v2 Solana payload. -func (c *BlockRunSolClient) signSolanaPayment(paymentHeaderB64 string) (string, error) { - if c.keypair == nil { - return "", fmt.Errorf("no private key set for BlockRun Sol wallet") - } - - decoded, err := X402DecodeHeader(paymentHeaderB64) - if err != nil { - return "", err - } - - var req X402v2PaymentRequired - if err := json.Unmarshal(decoded, &req); err != nil { - return "", fmt.Errorf("failed to parse x402 v2 Solana header: %w", err) - } - - // Find the Solana option - var opt *X402AcceptOption - for i := range req.Accepts { - if strings.HasPrefix(req.Accepts[i].Network, "solana:") { - opt = &req.Accepts[i] - break - } - } - if opt == nil { - return "", fmt.Errorf("no Solana payment option in x402 response") - } - - recipient := opt.PayTo - amount := opt.Amount - feePayer := "" - if opt.Extra != nil { - feePayer = opt.Extra["feePayer"] - } - if feePayer == "" { - return "", fmt.Errorf("feePayer missing from Solana x402 extra") - } - - maxTimeout := opt.MaxTimeoutSeconds - if maxTimeout == 0 { - maxTimeout = 300 - } - - resourceURL := DefaultBlockRunSolURL + BlockRunChatEndpoint - resourceDesc := "" - resourceMime := "application/json" - if req.Resource != nil { - resourceURL = req.Resource.URL - resourceDesc = req.Resource.Description - resourceMime = req.Resource.MimeType - } - - // Build the SPL TransferChecked transaction - txB64, err := c.buildSolanaTransferTx(recipient, feePayer, amount) - if err != nil { - return "", fmt.Errorf("failed to build Solana transfer tx: %w", err) - } - - // Build x402 v2 payment payload - paymentData := map[string]interface{}{ - "x402Version": 2, - "resource": map[string]string{ - "url": resourceURL, - "description": resourceDesc, - "mimeType": resourceMime, - }, - "accepted": map[string]interface{}{ - "scheme": "exact", - "network": SolanaNetwork, - "amount": amount, - "asset": SolanaUSDCMint, - "payTo": recipient, - "maxTimeoutSeconds": maxTimeout, - "extra": opt.Extra, - }, - "payload": map[string]string{ - "transaction": txB64, - }, - "extensions": map[string]interface{}{}, - } - - resultJSON, err := json.Marshal(paymentData) - if err != nil { - return "", fmt.Errorf("failed to marshal Solana payment: %w", err) - } - - return base64.StdEncoding.EncodeToString(resultJSON), nil -} - -// buildSolanaTransferTx builds a partial-signed VersionedTransaction for SPL USDC TransferChecked. -func (c *BlockRunSolClient) buildSolanaTransferTx(recipient, feePayer, amountStr string) (string, error) { - ownerPubkey := c.keypair.PublicKey() - - recipientPK, err := solana.PublicKeyFromBase58(recipient) - if err != nil { - return "", fmt.Errorf("invalid recipient address: %w", err) - } - feePayerPK, err := solana.PublicKeyFromBase58(feePayer) - if err != nil { - return "", fmt.Errorf("invalid feePayer address: %w", err) - } - mintPK := solana.MustPublicKeyFromBase58(SolanaUSDCMint) - - var amountU64 uint64 - if _, err := fmt.Sscanf(amountStr, "%d", &amountU64); err != nil { - return "", fmt.Errorf("invalid amount %q: %w", amountStr, err) - } - - sourceATA, _, err := solana.FindAssociatedTokenAddress(ownerPubkey, mintPK) - if err != nil { - return "", fmt.Errorf("failed to derive source ATA: %w", err) - } - destATA, _, err := solana.FindAssociatedTokenAddress(recipientPK, mintPK) - if err != nil { - return "", fmt.Errorf("failed to derive dest ATA: %w", err) - } - - rpcClient := rpc.New(SolanaMainnetRPC) - bhResp, err := rpcClient.GetLatestBlockhash(context.Background(), rpc.CommitmentFinalized) - if err != nil { - return "", fmt.Errorf("failed to fetch blockhash: %w", err) - } - recentBlockhash := bhResp.Value.Blockhash - - setLimitIx, err := computebudget.NewSetComputeUnitLimitInstruction(computeUnitLimit).ValidateAndBuild() - if err != nil { - return "", fmt.Errorf("failed to build SetComputeUnitLimit: %w", err) - } - setPriceIx, err := computebudget.NewSetComputeUnitPriceInstruction(computeUnitPrice).ValidateAndBuild() - if err != nil { - return "", fmt.Errorf("failed to build SetComputeUnitPrice: %w", err) - } - transferIx, err := token.NewTransferCheckedInstruction( - amountU64, - 6, // USDC decimals - sourceATA, - mintPK, - destATA, - ownerPubkey, - []solana.PublicKey{}, - ).ValidateAndBuild() - if err != nil { - return "", fmt.Errorf("failed to build TransferChecked: %w", err) - } - - tx, err := solana.NewTransaction( - []solana.Instruction{setLimitIx, setPriceIx, transferIx}, - recentBlockhash, - solana.TransactionPayer(feePayerPK), - ) - if err != nil { - return "", fmt.Errorf("failed to build transaction: %w", err) - } - - _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { - if key.Equals(ownerPubkey) { - return &c.keypair - } - return nil // feePayer will be signed by BlockRun CDP - }) - if err != nil { - return "", fmt.Errorf("failed to sign transaction: %w", err) - } - - txBytes, err := tx.MarshalBinary() - if err != nil { - return "", fmt.Errorf("failed to serialize transaction: %w", err) - } - - return base64.StdEncoding.EncodeToString(txBytes), nil -} - -// BuildUrl returns the full BlockRun Solana endpoint URL. -func (c *BlockRunSolClient) BuildUrl() string { - return DefaultBlockRunSolURL + BlockRunChatEndpoint -} - -func (c *BlockRunSolClient) BuildRequest(url string, jsonData []byte) (*http.Request, error) { - return X402BuildRequest(url, jsonData) -} diff --git a/mcp/payment/claw402.go b/mcp/payment/claw402.go index 9dc4a6b8..990fae7d 100644 --- a/mcp/payment/claw402.go +++ b/mcp/payment/claw402.go @@ -132,7 +132,7 @@ func (c *Claw402Client) CallWithRequestFull(req *mcp.Request) (*mcp.LLMResponse, return X402CallFull(c.Client, c.signPayment, "Claw402", req) } -// signPayment signs x402 v2 EIP-712 payment (same Base chain + USDC as BlockRunBase). +// signPayment signs x402 v2 EIP-712 payment on Base chain + USDC. func (c *Claw402Client) signPayment(paymentHeaderB64 string) (string, error) { return SignBasePaymentHeader(c.privateKey, paymentHeaderB64, "Claw402") } diff --git a/mcp/payment/x402.go b/mcp/payment/x402.go index 7649de11..064cc086 100644 --- a/mcp/payment/x402.go +++ b/mcp/payment/x402.go @@ -4,14 +4,19 @@ import ( "bytes" "context" "crypto/ecdsa" + "crypto/rand" "encoding/base64" + "encoding/hex" "encoding/json" "fmt" "io" + "math/big" "net/http" + "strings" "time" "github.com/ethereum/go-ethereum/crypto" + "golang.org/x/crypto/sha3" "nofx/mcp" ) @@ -77,7 +82,7 @@ func X402DecodeHeader(b64 string) ([]byte, error) { } // SignBasePaymentHeader decodes a base64 x402 header, parses it, and signs with -// EIP-712 (USDC TransferWithAuthorization). Shared by BlockRunBase and Claw402. +// EIP-712 (USDC TransferWithAuthorization). func SignBasePaymentHeader(privateKey *ecdsa.PrivateKey, paymentHeaderB64 string, providerName string) (string, error) { if privateKey == nil { return "", fmt.Errorf("no private key set for %s wallet", providerName) @@ -521,3 +526,252 @@ func X402CallFull(c *mcp.Client, signFn X402SignFunc, tag string, req *mcp.Reque } return c.Hooks.ParseMCPResponseFull(body) } + +// ── Shared EIP-712 constants & helpers (Base chain, USDC) ──────────────────── + +const ( + BaseUSDCContract = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" + BaseChainID int64 = 8453 + BaseNetwork = "eip155:8453" +) + +// EIP-712 type hashes for USDC TransferWithAuthorization (ERC-3009) +var ( + eip712DomainTypeHash = keccak256String("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") + transferWithAuthTypeHash = keccak256String("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)") +) + +func keccak256String(s string) []byte { + h := sha3.NewLegacyKeccak256() + h.Write([]byte(s)) + return h.Sum(nil) +} + +func keccak256Bytes(data ...[]byte) []byte { + h := sha3.NewLegacyKeccak256() + for _, b := range data { + h.Write(b) + } + return h.Sum(nil) +} + +// SignX402Payment is the shared EIP-712 signing logic for x402 v2 on Base USDC. +func SignX402Payment(privateKey *ecdsa.PrivateKey, senderAddr string, opt X402AcceptOption, resource *X402Resource) (string, error) { + recipient := opt.PayTo + amount := opt.Amount + network := opt.Network + asset := opt.Asset + extra := opt.Extra + maxTimeout := opt.MaxTimeoutSeconds + if maxTimeout == 0 { + maxTimeout = 300 + } + + resourceURL := "" + resourceDesc := "" + resourceMime := "application/json" + if resource != nil { + resourceURL = resource.URL + resourceDesc = resource.Description + resourceMime = resource.MimeType + } + + now := time.Now().Unix() + validAfter := int64(0) + validBefore := now + int64(maxTimeout) + + 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) + + domainName := "USD Coin" + domainVersion := "2" + if extra != nil { + if v, ok := extra["name"]; ok && v != "" { + domainName = v + } + if v, ok := extra["version"]; ok && v != "" { + domainVersion = v + } + } + + domainSeparator, err := buildDomainSeparatorDynamic(domainName, domainVersion, network, asset) + if err != nil { + return "", fmt.Errorf("failed to build domain separator: %w", err) + } + + amountBig, err := parseBigInt(amount) + if err != nil { + return "", fmt.Errorf("invalid amount: %w", err) + } + + structHash, err := buildTransferWithAuthHashDynamic(senderAddr, recipient, amountBig, validAfter, validBefore, nonce) + if err != nil { + return "", fmt.Errorf("failed to build struct hash: %w", err) + } + + digest := make([]byte, 0, 66) + digest = append(digest, 0x19, 0x01) + digest = append(digest, domainSeparator...) + digest = append(digest, structHash...) + hash := keccak256Bytes(digest) + + sig, err := crypto.Sign(hash, privateKey) + if err != nil { + return "", fmt.Errorf("failed to sign: %w", err) + } + if sig[64] < 27 { + sig[64] += 27 + } + + sigHex := "0x" + hex.EncodeToString(sig) + + paymentData := map[string]interface{}{ + "x402Version": 2, + "resource": map[string]string{ + "url": resourceURL, + "description": resourceDesc, + "mimeType": resourceMime, + }, + "accepted": map[string]interface{}{ + "scheme": "exact", + "network": network, + "amount": amount, + "asset": asset, + "payTo": recipient, + "maxTimeoutSeconds": maxTimeout, + "extra": extra, + }, + "payload": map[string]interface{}{ + "signature": sigHex, + "authorization": map[string]string{ + "from": senderAddr, + "to": recipient, + "value": amount, + "validAfter": fmt.Sprintf("%d", validAfter), + "validBefore": fmt.Sprintf("%d", validBefore), + "nonce": nonce, + }, + }, + "extensions": map[string]interface{}{}, + } + + resultJSON, err := json.Marshal(paymentData) + if err != nil { + return "", fmt.Errorf("failed to marshal payment result: %w", err) + } + + return base64.StdEncoding.EncodeToString(resultJSON), nil +} + +// buildDomainSeparatorDynamic builds the EIP-712 domain separator using runtime values. +func buildDomainSeparatorDynamic(name, version, network, asset string) ([]byte, error) { + chainID := new(big.Int).SetInt64(BaseChainID) + if strings.HasPrefix(network, "eip155:") { + parts := strings.SplitN(network, ":", 2) + if len(parts) == 2 { + if n, ok := new(big.Int).SetString(parts[1], 10); ok { + chainID = n + } + } + } + + contractAddr, err := hex.DecodeString(strings.TrimPrefix(asset, "0x")) + if err != nil { + return nil, fmt.Errorf("invalid contract address: %w", err) + } + + nameHash := keccak256String(name) + versionHash := keccak256String(version) + + encoded := make([]byte, 0, 5*32) + encoded = append(encoded, leftPad32(eip712DomainTypeHash)...) + encoded = append(encoded, leftPad32(nameHash)...) + encoded = append(encoded, leftPad32(versionHash)...) + encoded = append(encoded, leftPad32(chainID.Bytes())...) + addrPadded := make([]byte, 32) + copy(addrPadded[32-len(contractAddr):], contractAddr) + encoded = append(encoded, addrPadded...) + + return keccak256Bytes(encoded), nil +} + +// buildTransferWithAuthHashDynamic builds the struct hash for TransferWithAuthorization. +func buildTransferWithAuthHashDynamic(from, to string, value *big.Int, validAfter, validBefore int64, nonce string) ([]byte, error) { + fromBytes, err := hexToAddress(from) + if err != nil { + return nil, fmt.Errorf("invalid from address: %w", err) + } + toBytes, err := hexToAddress(to) + if err != nil { + return nil, fmt.Errorf("invalid to address: %w", err) + } + nonceBytes, err := hexToBytes32(nonce) + if err != nil { + return nil, fmt.Errorf("invalid nonce: %w", err) + } + + validAfterBig := new(big.Int).SetInt64(validAfter) + validBeforeBig := new(big.Int).SetInt64(validBefore) + + encoded := make([]byte, 0, 7*32) + encoded = append(encoded, leftPad32(transferWithAuthTypeHash)...) + encoded = append(encoded, leftPad32(fromBytes)...) + encoded = append(encoded, leftPad32(toBytes)...) + encoded = append(encoded, leftPad32(value.Bytes())...) + encoded = append(encoded, leftPad32(validAfterBig.Bytes())...) + encoded = append(encoded, leftPad32(validBeforeBig.Bytes())...) + encoded = append(encoded, leftPad32(nonceBytes)...) + + return keccak256Bytes(encoded), nil +} + +func hexToAddress(s string) ([]byte, error) { + s = strings.TrimPrefix(s, "0x") + b, err := hex.DecodeString(s) + if err != nil { + return nil, err + } + if len(b) != 20 { + return nil, fmt.Errorf("address must be 20 bytes, got %d", len(b)) + } + return b, nil +} + +func hexToBytes32(s string) ([]byte, error) { + s = strings.TrimPrefix(s, "0x") + b, err := hex.DecodeString(s) + if err != nil { + return nil, err + } + if len(b) > 32 { + return nil, fmt.Errorf("nonce too long: %d bytes", len(b)) + } + return b, nil +} + +func parseBigInt(s string) (*big.Int, error) { + n := new(big.Int) + if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { + if _, ok := n.SetString(s[2:], 16); ok { + return n, nil + } + return nil, fmt.Errorf("cannot parse hex big.Int from %q", s) + } + if _, ok := n.SetString(s, 10); ok { + return n, nil + } + return nil, fmt.Errorf("cannot parse big.Int from %q", s) +} + +// leftPad32 pads a byte slice to 32 bytes on the left (ABI encoding). +func leftPad32(b []byte) []byte { + if len(b) >= 32 { + return b[:32] + } + padded := make([]byte, 32) + copy(padded[32-len(b):], b) + return padded +} diff --git a/mcp/providers.go b/mcp/providers.go index ad7f2eed..75f3d9c7 100644 --- a/mcp/providers.go +++ b/mcp/providers.go @@ -13,9 +13,7 @@ const ( ProviderKimi = "kimi" ProviderMiniMax = "minimax" - ProviderBlockRunBase = "blockrun-base" - ProviderBlockRunSol = "blockrun-sol" - ProviderClaw402 = "claw402" + ProviderClaw402 = "claw402" // Default DeepSeek configuration (used as fallback in NewClient) DefaultDeepSeekBaseURL = "https://api.deepseek.com" diff --git a/telegram/bot.go b/telegram/bot.go index 95085257..6cf81dc6 100644 --- a/telegram/bot.go +++ b/telegram/bot.go @@ -317,7 +317,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" || provider == "claw402" + return provider == "claw402" } func clientForProvider(provider string) mcp.AIClient { diff --git a/telemetry/experience.go b/telemetry/experience.go index 8fae9d79..0e4e5580 100644 --- a/telemetry/experience.go +++ b/telemetry/experience.go @@ -42,7 +42,7 @@ type AIUsageEvent struct { TraderID string ModelProvider string // openai, deepseek, anthropic, etc. ModelName string // gpt-4o, deepseek-chat, claude-3, etc. - Channel string // payment channel: "claw402", "blockrun", or "native" + Channel string // payment channel: "claw402" or "native" InputTokens int OutputTokens int } diff --git a/trader/auto_trader.go b/trader/auto_trader.go index dddd011c..5874af02 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -205,9 +205,9 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au mcpClient = mcp.New() } - // Payment providers (blockrun-*, claw402) ignore customURL + // Payment providers (claw402) ignore customURL switch aiModel { - case "blockrun-base", "blockrun-sol", "claw402": + case "claw402": mcpClient.SetAPIKey(apiKey, "", config.CustomModelName) default: mcpClient.SetAPIKey(apiKey, customURL, config.CustomModelName) diff --git a/web/public/icons/blockrun.svg b/web/public/icons/blockrun.svg deleted file mode 100644 index 403f0a5a..00000000 --- a/web/public/icons/blockrun.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/web/src/components/common/ModelIcons.tsx b/web/src/components/common/ModelIcons.tsx index e51a3e7e..f7a0fe08 100644 --- a/web/src/components/common/ModelIcons.tsx +++ b/web/src/components/common/ModelIcons.tsx @@ -14,8 +14,6 @@ const MODEL_COLORS: Record = { grok: '#000000', openai: '#10A37F', minimax: '#E45735', - 'blockrun-base': '#2563EB', - 'blockrun-sol': '#9945FF', claw402: '#7C3AED', } @@ -51,10 +49,6 @@ export const getModelIcon = (modelType: string, props: IconProps = {}) => { case 'minimax': iconPath = '/icons/minimax.svg' break - case 'blockrun-base': - case 'blockrun-sol': - iconPath = '/icons/blockrun.svg' - break case 'claw402': iconPath = '/icons/claw402.png' break diff --git a/web/src/components/trader/ModelConfigModal.tsx b/web/src/components/trader/ModelConfigModal.tsx index dd1b1a11..7b8780a9 100644 --- a/web/src/components/trader/ModelConfigModal.tsx +++ b/web/src/components/trader/ModelConfigModal.tsx @@ -8,7 +8,6 @@ import { getModelIcon } from '../common/ModelIcons' import { ModelStepIndicator } from './ModelStepIndicator' import { ModelCard } from './ModelCard' import { - BLOCKRUN_MODELS, CLAW402_MODELS, AI_PROVIDER_CONFIG, getShortName, @@ -240,7 +239,7 @@ function ModelSelectionStep({ )}

- {availableModels.filter(m => !m.provider?.startsWith('blockrun') && m.provider !== 'claw402').map((model) => ( + {availableModels.filter(m => m.provider !== 'claw402').map((model) => ( ))}
- {availableModels.some(m => m.provider?.startsWith('blockrun')) && ( - <> -
-
- - {t('modelConfig.viaBlockrunWallet', language)} - -
-
-
- {availableModels.filter(m => m.provider?.startsWith('blockrun')).map((model) => ( - onSelectModel(model.id)} - configured={configuredIds.has(model.id)} - /> - ))} -
- - )}
{t('modelConfig.modelsConfigured', language)}
@@ -800,9 +777,7 @@ function StandardProviderConfigForm({ > - {selectedModel.provider?.startsWith('blockrun') - ? t('modelConfig.getStarted', language) - : t('modelConfig.getApiKey', language)} + {t('modelConfig.getApiKey', language)} )} @@ -826,106 +801,61 @@ function StandardProviderConfigForm({ - {selectedModel.provider?.startsWith('blockrun') - ? t('modelConfig.walletPrivateKeyLabel', language) - : 'API Key *'} + {'API Key *'} onApiKeyChange(e.target.value)} - placeholder={ - selectedModel.provider === 'blockrun-base' - ? '0x... (EVM private key)' - : selectedModel.provider === 'blockrun-sol' - ? 'bs58 encoded key (Solana)' - : t('enterAPIKey', language) - } + placeholder={t('enterAPIKey', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
- {/* Custom Base URL (hidden for BlockRun) */} - {!selectedModel.provider?.startsWith('blockrun') && ( -
- - onBaseUrlChange(e.target.value)} - placeholder={t('customBaseURLPlaceholder', language)} - className="w-full px-4 py-3 rounded-xl" - style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} - /> -
- {t('leaveBlankForDefault', language)} -
+ {/* Custom Base URL */} +
+ + onBaseUrlChange(e.target.value)} + placeholder={t('customBaseURLPlaceholder', language)} + className="w-full px-4 py-3 rounded-xl" + style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} + /> +
+ {t('leaveBlankForDefault', language)}
- )} +
- {/* Custom Model Name (hidden for BlockRun) */} - {!selectedModel.provider?.startsWith('blockrun') && ( -
- - onModelNameChange(e.target.value)} - placeholder={t('customModelNamePlaceholder', language)} - className="w-full px-4 py-3 rounded-xl" - style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} - /> -
- {t('leaveBlankForDefaultModel', language)} -
+ {/* Custom Model Name */} +
+ + onModelNameChange(e.target.value)} + placeholder={t('customModelNamePlaceholder', language)} + className="w-full px-4 py-3 rounded-xl" + style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} + /> +
+ {t('leaveBlankForDefaultModel', language)}
- )} +
- {/* BlockRun Model Selector */} - {selectedModel.provider?.startsWith('blockrun') && ( -
- -
- {BLOCKRUN_MODELS.map((m) => { - const isSelected = (modelName || BLOCKRUN_MODELS[0].id) === m.id - return ( - - ) - })} -
-
- )} {/* Info Box */}
diff --git a/web/src/components/trader/model-constants.ts b/web/src/components/trader/model-constants.ts index cfca87ab..cb741e27 100644 --- a/web/src/components/trader/model-constants.ts +++ b/web/src/components/trader/model-constants.ts @@ -1,11 +1,5 @@ // Constants for AI model and provider configuration -export interface BlockrunModel { - id: string - name: string - desc: string -} - export interface Claw402Model { id: string name: string @@ -41,16 +35,6 @@ export function getShortName(fullName: string): string { return parts.length > 1 ? parts[parts.length - 1] : fullName } -// Top models available through BlockRun wallet providers -export const BLOCKRUN_MODELS: BlockrunModel[] = [ - { id: 'gpt-5.4', name: 'GPT-5.4', desc: 'OpenAI · Flagship' }, - { id: 'claude-opus-4.6', name: 'Claude Opus 4.6', desc: 'Anthropic · Flagship' }, - { id: 'gemini-3.1-pro', name: 'Gemini 3.1 Pro', desc: 'Google · Flagship' }, - { id: 'grok-3', name: 'Grok 3', desc: 'xAI · Flagship' }, - { id: 'deepseek-chat', name: 'DeepSeek Chat', desc: 'DeepSeek · Flagship' }, - { id: 'minimax-m2.5', name: 'MiniMax M2.5', desc: 'MiniMax · Flagship' }, -] - // Models available through Claw402 (x402 USDC payment protocol) export const CLAW402_MODELS: Claw402Model[] = [ { id: 'deepseek', name: 'DeepSeek V3', provider: 'DeepSeek', desc: '$0.003/call', icon: '🔥', price: 0.003 }, @@ -116,16 +100,6 @@ export const AI_PROVIDER_CONFIG: Record = { apiUrl: 'https://claw402.ai', apiName: 'Claw402', }, - 'blockrun-base': { - defaultModel: 'gpt-5.4', - apiUrl: 'https://blockrun.ai', - apiName: 'BlockRun', - }, - 'blockrun-sol': { - defaultModel: 'gpt-5.4', - apiUrl: 'https://sol.blockrun.ai', - apiName: 'BlockRun', - }, } // Helper function to get exchange display name from exchange ID (UUID) diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index 25c89a5e..d524fad6 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -1224,7 +1224,6 @@ export const translations = { fundStep3: '$5-10 USDC lasts a long time (~$0.003/call)', back: 'Back', startTrading: 'Start Trading', - viaBlockrunWallet: 'Via BlockRun Wallet', modelsConfigured: 'Models with gold badge are already configured', getStarted: 'Get Started', getApiKey: 'Get API Key', @@ -2513,7 +2512,6 @@ export const translations = { fundStep3: '充入 $5-10 USDC 即可使用很长时间(约 $0.003/次调用)', back: '返回', startTrading: '开始交易', - viaBlockrunWallet: '通过钱包支付', modelsConfigured: '带金色标记的模型已配置', getStarted: '开始使用', getApiKey: '获取 API Key', @@ -3607,7 +3605,6 @@ export const translations = { fundStep3: '$5-10 USDC cukup untuk waktu lama (~$0.003/panggilan)', back: 'Kembali', startTrading: 'Mulai Trading', - viaBlockrunWallet: 'Via BlockRun Wallet', modelsConfigured: 'Model dengan lencana emas sudah dikonfigurasi', getStarted: 'Mulai', getApiKey: 'Dapatkan API Key',