From 966995fb888e0094a0986ab8e1eaabf77505bf7b Mon Sep 17 00:00:00 2001
From: tinkle-community
-
@@ -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 @@
-
@@ -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 @@
-
@@ -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 @@
-
@@ -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 @@
-
@@ -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 @@
-
@@ -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 @@
-
@@ -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