mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 01:48:22 +08:00
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
This commit is contained in:
@@ -17,7 +17,6 @@
|
|||||||
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||||
<a href="https://x402.org"><img src="https://img.shields.io/badge/x402-USDC%20Payments-2775CA?style=flat" alt="x402"></a>
|
<a href="https://x402.org"><img src="https://img.shields.io/badge/x402-USDC%20Payments-2775CA?style=flat" alt="x402"></a>
|
||||||
<a href="https://claw402.ai"><img src="https://img.shields.io/badge/Claw402-AI%20Gateway-FF6B35?style=flat" alt="Claw402"></a>
|
<a href="https://claw402.ai"><img src="https://img.shields.io/badge/Claw402-AI%20Gateway-FF6B35?style=flat" alt="Claw402"></a>
|
||||||
<a href="https://blockrun.ai"><img src="https://img.shields.io/badge/BlockRun-x402%20Provider-8B5CF6?style=flat" alt="BlockRun"></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -63,10 +62,6 @@ No accounts. No API keys. No prepaid credits. One wallet, every model.
|
|||||||
| Provider | Chain | Models |
|
| Provider | Chain | Models |
|
||||||
|:---------|:------|:-------|
|
|:---------|:------|:-------|
|
||||||
| <img src="web/public/icons/claw402.png" width="20" height="20" style="vertical-align: middle;"/> **[Claw402](https://claw402.ai)** | Base | GPT-5.4, Claude Opus, DeepSeek, Qwen, Grok, Gemini, Kimi — 15+ models |
|
| <img src="web/public/icons/claw402.png" width="20" height="20" style="vertical-align: middle;"/> **[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)
|
### 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 │
|
│ MCP AI Client Layer │
|
||||||
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
||||||
│ │ API Key │ │ x402 │ │ ClawRouter│ │
|
│ │ API Key │ │ x402 │ │ │ │
|
||||||
│ │ DeepSeek │ │ Claw402 │ │ 41+ models│ │
|
│ │ DeepSeek │ │ Claw402 │ │ │ │
|
||||||
│ │ GPT,Claude │ │ BlockRun │ │ auto-route│ │
|
│ │ GPT,Claude │ │ │ │ │ │
|
||||||
│ └───────────┘ └───────────┘ └───────────┘ │
|
│ └───────────┘ └───────────┘ └───────────┘ │
|
||||||
├─────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────┤
|
||||||
│ Exchange Connectors │
|
│ Exchange Connectors │
|
||||||
|
|||||||
@@ -202,8 +202,6 @@ func (s *Server) handleGetSupportedModels(c *gin.Context) {
|
|||||||
{"id": "grok", "name": "Grok (xAI)", "provider": "grok", "defaultModel": "grok-3-latest"},
|
{"id": "grok", "name": "Grok (xAI)", "provider": "grok", "defaultModel": "grok-3-latest"},
|
||||||
{"id": "kimi", "name": "Kimi (Moonshot)", "provider": "kimi", "defaultModel": "moonshot-v1-auto"},
|
{"id": "kimi", "name": "Kimi (Moonshot)", "provider": "kimi", "defaultModel": "moonshot-v1-auto"},
|
||||||
{"id": "minimax", "name": "MiniMax", "provider": "minimax", "defaultModel": "MiniMax-M2.5"},
|
{"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"},
|
{"id": "claw402", "name": "Claw402 (Base USDC)", "provider": "claw402", "defaultModel": "deepseek"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -650,7 +650,7 @@ func (s *Server) runRealAITest(userID, modelID, systemPrompt, userPrompt string)
|
|||||||
|
|
||||||
// Payment providers ignore custom URL
|
// Payment providers ignore custom URL
|
||||||
switch provider {
|
switch provider {
|
||||||
case "blockrun-base", "blockrun-sol", "claw402":
|
case "claw402":
|
||||||
aiClient.SetAPIKey(apiKey, "", model.CustomModelName)
|
aiClient.SetAPIKey(apiKey, "", model.CustomModelName)
|
||||||
default:
|
default:
|
||||||
aiClient.SetAPIKey(apiKey, model.CustomAPIURL, model.CustomModelName)
|
aiClient.SetAPIKey(apiKey, model.CustomAPIURL, model.CustomModelName)
|
||||||
|
|||||||
@@ -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
|
## 🔑 Prerequisites
|
||||||
|
|
||||||
Before starting, ensure you have:
|
Before starting, ensure you have:
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||||
<a href="https://x402.org"><img src="https://img.shields.io/badge/x402-USDC%20Payments-2775CA?style=flat" alt="x402"></a>
|
<a href="https://x402.org"><img src="https://img.shields.io/badge/x402-USDC%20Payments-2775CA?style=flat" alt="x402"></a>
|
||||||
<a href="https://claw402.ai"><img src="https://img.shields.io/badge/Claw402-AI%20Gateway-FF6B35?style=flat" alt="Claw402"></a>
|
<a href="https://claw402.ai"><img src="https://img.shields.io/badge/Claw402-AI%20Gateway-FF6B35?style=flat" alt="Claw402"></a>
|
||||||
<a href="https://blockrun.ai"><img src="https://img.shields.io/badge/BlockRun-x402%20Provider-8B5CF6?style=flat" alt="BlockRun"></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -63,10 +62,6 @@ x402 フロー:
|
|||||||
| プロバイダー | チェーン | モデル |
|
| プロバイダー | チェーン | モデル |
|
||||||
|:---------|:------|:-------|
|
|:---------|:------|:-------|
|
||||||
| <img src="../../../web/public/icons/claw402.png" width="20" height="20" style="vertical-align: middle;"/> **[Claw402](https://claw402.ai)** | Base | GPT-5.4, Claude Opus, DeepSeek, Qwen, Grok, Gemini, Kimi — 15+ モデル |
|
| <img src="../../../web/public/icons/claw402.png" width="20" height="20" style="vertical-align: middle;"/> **[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 キー不要)
|
### AI モデル (x402 モード — API キー不要)
|
||||||
|
|
||||||
15+ モデルを [Claw402](https://claw402.ai) または [BlockRun](https://blockrun.ai) 経由で利用 — USDC ウォレットのみ
|
15+ モデルを [Claw402](https://claw402.ai) 経由で利用 — USDC ウォレットのみ
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||||
<a href="https://x402.org"><img src="https://img.shields.io/badge/x402-USDC%20Payments-2775CA?style=flat" alt="x402"></a>
|
<a href="https://x402.org"><img src="https://img.shields.io/badge/x402-USDC%20Payments-2775CA?style=flat" alt="x402"></a>
|
||||||
<a href="https://claw402.ai"><img src="https://img.shields.io/badge/Claw402-AI%20Gateway-FF6B35?style=flat" alt="Claw402"></a>
|
<a href="https://claw402.ai"><img src="https://img.shields.io/badge/Claw402-AI%20Gateway-FF6B35?style=flat" alt="Claw402"></a>
|
||||||
<a href="https://blockrun.ai"><img src="https://img.shields.io/badge/BlockRun-x402%20Provider-8B5CF6?style=flat" alt="BlockRun"></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -63,10 +62,6 @@ x402 플로우:
|
|||||||
| 프로바이더 | 체인 | 모델 |
|
| 프로바이더 | 체인 | 모델 |
|
||||||
|:---------|:------|:-------|
|
|:---------|:------|:-------|
|
||||||
| <img src="../../../web/public/icons/claw402.png" width="20" height="20" style="vertical-align: middle;"/> **[Claw402](https://claw402.ai)** | Base | GPT-5.4, Claude Opus, DeepSeek, Qwen, Grok, Gemini, Kimi — 15+ 모델 |
|
| <img src="../../../web/public/icons/claw402.png" width="20" height="20" style="vertical-align: middle;"/> **[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 키 불필요)
|
### AI 모델 (x402 모드 — API 키 불필요)
|
||||||
|
|
||||||
15+ 모델을 [Claw402](https://claw402.ai) 또는 [BlockRun](https://blockrun.ai)으로 이용 — USDC 지갑만 있으면 됩니다
|
15+ 모델을 [Claw402](https://claw402.ai)로 이용 — USDC 지갑만 있으면 됩니다
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||||
<a href="https://x402.org"><img src="https://img.shields.io/badge/x402-USDC%20Payments-2775CA?style=flat" alt="x402"></a>
|
<a href="https://x402.org"><img src="https://img.shields.io/badge/x402-USDC%20Payments-2775CA?style=flat" alt="x402"></a>
|
||||||
<a href="https://claw402.ai"><img src="https://img.shields.io/badge/Claw402-AI%20Gateway-FF6B35?style=flat" alt="Claw402"></a>
|
<a href="https://claw402.ai"><img src="https://img.shields.io/badge/Claw402-AI%20Gateway-FF6B35?style=flat" alt="Claw402"></a>
|
||||||
<a href="https://blockrun.ai"><img src="https://img.shields.io/badge/BlockRun-x402%20Provider-8B5CF6?style=flat" alt="BlockRun"></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -63,10 +62,6 @@ x402 процесс:
|
|||||||
| Провайдер | Сеть | Модели |
|
| Провайдер | Сеть | Модели |
|
||||||
|:---------|:------|:-------|
|
|:---------|:------|:-------|
|
||||||
| <img src="../../../web/public/icons/claw402.png" width="20" height="20" style="vertical-align: middle;"/> **[Claw402](https://claw402.ai)** | Base | GPT-5.4, Claude Opus, DeepSeek, Qwen, Grok, Gemini, Kimi — 15+ моделей |
|
| <img src="../../../web/public/icons/claw402.png" width="20" height="20" style="vertical-align: middle;"/> **[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 ключей)
|
### AI Модели (Режим x402 — без API ключей)
|
||||||
|
|
||||||
15+ моделей через [Claw402](https://claw402.ai) или [BlockRun](https://blockrun.ai) — только USDC кошелёк
|
15+ моделей через [Claw402](https://claw402.ai) — только USDC кошелёк
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||||
<a href="https://x402.org"><img src="https://img.shields.io/badge/x402-USDC%20Payments-2775CA?style=flat" alt="x402"></a>
|
<a href="https://x402.org"><img src="https://img.shields.io/badge/x402-USDC%20Payments-2775CA?style=flat" alt="x402"></a>
|
||||||
<a href="https://claw402.ai"><img src="https://img.shields.io/badge/Claw402-AI%20Gateway-FF6B35?style=flat" alt="Claw402"></a>
|
<a href="https://claw402.ai"><img src="https://img.shields.io/badge/Claw402-AI%20Gateway-FF6B35?style=flat" alt="Claw402"></a>
|
||||||
<a href="https://blockrun.ai"><img src="https://img.shields.io/badge/BlockRun-x402%20Provider-8B5CF6?style=flat" alt="BlockRun"></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -63,10 +62,6 @@ x402 процес:
|
|||||||
| Провайдер | Мережа | Моделі |
|
| Провайдер | Мережа | Моделі |
|
||||||
|:---------|:------|:-------|
|
|:---------|:------|:-------|
|
||||||
| <img src="../../../web/public/icons/claw402.png" width="20" height="20" style="vertical-align: middle;"/> **[Claw402](https://claw402.ai)** | Base | GPT-5.4, Claude Opus, DeepSeek, Qwen, Grok, Gemini, Kimi — 15+ моделей |
|
| <img src="../../../web/public/icons/claw402.png" width="20" height="20" style="vertical-align: middle;"/> **[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 ключів)
|
### AI Моделі (Режим x402 — без API ключів)
|
||||||
|
|
||||||
15+ моделей через [Claw402](https://claw402.ai) або [BlockRun](https://blockrun.ai) — лише USDC гаманець
|
15+ моделей через [Claw402](https://claw402.ai) — лише USDC гаманець
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||||
<a href="https://x402.org"><img src="https://img.shields.io/badge/x402-USDC%20Payments-2775CA?style=flat" alt="x402"></a>
|
<a href="https://x402.org"><img src="https://img.shields.io/badge/x402-USDC%20Payments-2775CA?style=flat" alt="x402"></a>
|
||||||
<a href="https://claw402.ai"><img src="https://img.shields.io/badge/Claw402-AI%20Gateway-FF6B35?style=flat" alt="Claw402"></a>
|
<a href="https://claw402.ai"><img src="https://img.shields.io/badge/Claw402-AI%20Gateway-FF6B35?style=flat" alt="Claw402"></a>
|
||||||
<a href="https://blockrun.ai"><img src="https://img.shields.io/badge/BlockRun-x402%20Provider-8B5CF6?style=flat" alt="BlockRun"></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -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 |
|
| Nhà cung cấp | Chain | Mô hình |
|
||||||
|:---------|:------|:-------|
|
|:---------|:------|:-------|
|
||||||
| <img src="../../../web/public/icons/claw402.png" width="20" height="20" style="vertical-align: middle;"/> **[Claw402](https://claw402.ai)** | Base | GPT-5.4, Claude Opus, DeepSeek, Qwen, Grok, Gemini, Kimi — 15+ mô hình |
|
| <img src="../../../web/public/icons/claw402.png" width="20" height="20" style="vertical-align: middle;"/> **[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)
|
### 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||||
<a href="https://x402.org"><img src="https://img.shields.io/badge/x402-USDC%20Payments-2775CA?style=flat" alt="x402"></a>
|
<a href="https://x402.org"><img src="https://img.shields.io/badge/x402-USDC%20Payments-2775CA?style=flat" alt="x402"></a>
|
||||||
<a href="https://claw402.ai"><img src="https://img.shields.io/badge/Claw402-AI%20Gateway-FF6B35?style=flat" alt="Claw402"></a>
|
<a href="https://claw402.ai"><img src="https://img.shields.io/badge/Claw402-AI%20Gateway-FF6B35?style=flat" alt="Claw402"></a>
|
||||||
<a href="https://blockrun.ai"><img src="https://img.shields.io/badge/BlockRun-x402%20Provider-8B5CF6?style=flat" alt="BlockRun"></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -65,10 +64,6 @@ x402 流程:
|
|||||||
| 提供商 | 链 | 模型 |
|
| 提供商 | 链 | 模型 |
|
||||||
|:---------|:------|:-------|
|
|:---------|:------|:-------|
|
||||||
| <img src="../../../web/public/icons/claw402.png" width="20" height="20" style="vertical-align: middle;"/> **[Claw402](https://claw402.ai)** | Base | GPT-5.4、Claude Opus、DeepSeek、Qwen、Grok、Gemini、Kimi — 15+ 模型 |
|
| <img src="../../../web/public/icons/claw402.png" width="20" height="20" style="vertical-align: middle;"/> **[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)
|
### AI 模型 (x402 模式 — 无需 API Key)
|
||||||
|
|
||||||
15+ 模型通过 [Claw402](https://claw402.ai) 或 [BlockRun](https://blockrun.ai) 接入 — 只需一个 USDC 钱包
|
15+ 模型通过 [Claw402](https://claw402.ai) 接入 — 只需一个 USDC 钱包
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+2
-4
@@ -44,7 +44,7 @@ var (
|
|||||||
|
|
||||||
// TokenUsage represents token usage from AI API response
|
// TokenUsage represents token usage from AI API response
|
||||||
type TokenUsage struct {
|
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
|
Model string
|
||||||
PromptTokens int
|
PromptTokens int
|
||||||
CompletionTokens int
|
CompletionTokens int
|
||||||
@@ -52,13 +52,11 @@ type TokenUsage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Channel returns the payment channel category for telemetry.
|
// 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 {
|
func (u TokenUsage) Channel() string {
|
||||||
switch u.Provider {
|
switch u.Provider {
|
||||||
case ProviderClaw402:
|
case ProviderClaw402:
|
||||||
return "claw402"
|
return "claw402"
|
||||||
case ProviderBlockRunBase, ProviderBlockRunSol:
|
|
||||||
return "blockrun"
|
|
||||||
default:
|
default:
|
||||||
return "native"
|
return "native"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -132,7 +132,7 @@ func (c *Claw402Client) CallWithRequestFull(req *mcp.Request) (*mcp.LLMResponse,
|
|||||||
return X402CallFull(c.Client, c.signPayment, "Claw402", req)
|
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) {
|
func (c *Claw402Client) signPayment(paymentHeaderB64 string) (string, error) {
|
||||||
return SignBasePaymentHeader(c.privateKey, paymentHeaderB64, "Claw402")
|
return SignBasePaymentHeader(c.privateKey, paymentHeaderB64, "Claw402")
|
||||||
}
|
}
|
||||||
|
|||||||
+255
-1
@@ -4,14 +4,19 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
|
||||||
"nofx/mcp"
|
"nofx/mcp"
|
||||||
)
|
)
|
||||||
@@ -77,7 +82,7 @@ func X402DecodeHeader(b64 string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SignBasePaymentHeader decodes a base64 x402 header, parses it, and signs with
|
// SignBasePaymentHeader decodes a base64 x402 header, parses it, and signs with
|
||||||
// EIP-712 (USDC TransferWithAuthorization). Shared by BlockRunBase and Claw402.
|
// EIP-712 (USDC TransferWithAuthorization).
|
||||||
func SignBasePaymentHeader(privateKey *ecdsa.PrivateKey, paymentHeaderB64 string, providerName string) (string, error) {
|
func SignBasePaymentHeader(privateKey *ecdsa.PrivateKey, paymentHeaderB64 string, providerName string) (string, error) {
|
||||||
if privateKey == nil {
|
if privateKey == nil {
|
||||||
return "", fmt.Errorf("no private key set for %s wallet", providerName)
|
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)
|
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
|
||||||
|
}
|
||||||
|
|||||||
+1
-3
@@ -13,9 +13,7 @@ const (
|
|||||||
ProviderKimi = "kimi"
|
ProviderKimi = "kimi"
|
||||||
ProviderMiniMax = "minimax"
|
ProviderMiniMax = "minimax"
|
||||||
|
|
||||||
ProviderBlockRunBase = "blockrun-base"
|
ProviderClaw402 = "claw402"
|
||||||
ProviderBlockRunSol = "blockrun-sol"
|
|
||||||
ProviderClaw402 = "claw402"
|
|
||||||
|
|
||||||
// Default DeepSeek configuration (used as fallback in NewClient)
|
// Default DeepSeek configuration (used as fallback in NewClient)
|
||||||
DefaultDeepSeekBaseURL = "https://api.deepseek.com"
|
DefaultDeepSeekBaseURL = "https://api.deepseek.com"
|
||||||
|
|||||||
+1
-1
@@ -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).
|
// isUSDCProvider returns true for providers that pay per call with USDC (x402 protocol).
|
||||||
func isUSDCProvider(provider string) bool {
|
func isUSDCProvider(provider string) bool {
|
||||||
return provider == "blockrun-base" || provider == "blockrun-sol" || provider == "claw402"
|
return provider == "claw402"
|
||||||
}
|
}
|
||||||
|
|
||||||
func clientForProvider(provider string) mcp.AIClient {
|
func clientForProvider(provider string) mcp.AIClient {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ type AIUsageEvent struct {
|
|||||||
TraderID string
|
TraderID string
|
||||||
ModelProvider string // openai, deepseek, anthropic, etc.
|
ModelProvider string // openai, deepseek, anthropic, etc.
|
||||||
ModelName string // gpt-4o, deepseek-chat, claude-3, 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
|
InputTokens int
|
||||||
OutputTokens int
|
OutputTokens int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,9 +205,9 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au
|
|||||||
mcpClient = mcp.New()
|
mcpClient = mcp.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payment providers (blockrun-*, claw402) ignore customURL
|
// Payment providers (claw402) ignore customURL
|
||||||
switch aiModel {
|
switch aiModel {
|
||||||
case "blockrun-base", "blockrun-sol", "claw402":
|
case "claw402":
|
||||||
mcpClient.SetAPIKey(apiKey, "", config.CustomModelName)
|
mcpClient.SetAPIKey(apiKey, "", config.CustomModelName)
|
||||||
default:
|
default:
|
||||||
mcpClient.SetAPIKey(apiKey, customURL, config.CustomModelName)
|
mcpClient.SetAPIKey(apiKey, customURL, config.CustomModelName)
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect x="30" y="20" width="55" height="60" rx="14" stroke="#FFFFFF" stroke-width="8" fill="none"/>
|
|
||||||
<path d="M15 35 L25 35" stroke="#FFFFFF" stroke-width="6" stroke-linecap="round"/>
|
|
||||||
<path d="M10 50 L25 50" stroke="#FFFFFF" stroke-width="6" stroke-linecap="round"/>
|
|
||||||
<path d="M15 65 L25 65" stroke="#FFFFFF" stroke-width="6" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 438 B |
@@ -14,8 +14,6 @@ const MODEL_COLORS: Record<string, string> = {
|
|||||||
grok: '#000000',
|
grok: '#000000',
|
||||||
openai: '#10A37F',
|
openai: '#10A37F',
|
||||||
minimax: '#E45735',
|
minimax: '#E45735',
|
||||||
'blockrun-base': '#2563EB',
|
|
||||||
'blockrun-sol': '#9945FF',
|
|
||||||
claw402: '#7C3AED',
|
claw402: '#7C3AED',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,10 +49,6 @@ export const getModelIcon = (modelType: string, props: IconProps = {}) => {
|
|||||||
case 'minimax':
|
case 'minimax':
|
||||||
iconPath = '/icons/minimax.svg'
|
iconPath = '/icons/minimax.svg'
|
||||||
break
|
break
|
||||||
case 'blockrun-base':
|
|
||||||
case 'blockrun-sol':
|
|
||||||
iconPath = '/icons/blockrun.svg'
|
|
||||||
break
|
|
||||||
case 'claw402':
|
case 'claw402':
|
||||||
iconPath = '/icons/claw402.png'
|
iconPath = '/icons/claw402.png'
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { getModelIcon } from '../common/ModelIcons'
|
|||||||
import { ModelStepIndicator } from './ModelStepIndicator'
|
import { ModelStepIndicator } from './ModelStepIndicator'
|
||||||
import { ModelCard } from './ModelCard'
|
import { ModelCard } from './ModelCard'
|
||||||
import {
|
import {
|
||||||
BLOCKRUN_MODELS,
|
|
||||||
CLAW402_MODELS,
|
CLAW402_MODELS,
|
||||||
AI_PROVIDER_CONFIG,
|
AI_PROVIDER_CONFIG,
|
||||||
getShortName,
|
getShortName,
|
||||||
@@ -240,7 +239,7 @@ function ModelSelectionStep({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-3 sm:grid-cols-4 gap-3">
|
<div className="grid grid-cols-3 sm:grid-cols-4 gap-3">
|
||||||
{availableModels.filter(m => !m.provider?.startsWith('blockrun') && m.provider !== 'claw402').map((model) => (
|
{availableModels.filter(m => m.provider !== 'claw402').map((model) => (
|
||||||
<ModelCard
|
<ModelCard
|
||||||
key={model.id}
|
key={model.id}
|
||||||
model={model}
|
model={model}
|
||||||
@@ -250,28 +249,6 @@ function ModelSelectionStep({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{availableModels.some(m => m.provider?.startsWith('blockrun')) && (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-3 pt-2">
|
|
||||||
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
|
|
||||||
<span className="text-xs font-medium px-2" style={{ color: '#848E9C' }}>
|
|
||||||
{t('modelConfig.viaBlockrunWallet', language)}
|
|
||||||
</span>
|
|
||||||
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
{availableModels.filter(m => m.provider?.startsWith('blockrun')).map((model) => (
|
|
||||||
<ModelCard
|
|
||||||
key={model.id}
|
|
||||||
model={model}
|
|
||||||
selected={selectedModelId === model.id}
|
|
||||||
onClick={() => onSelectModel(model.id)}
|
|
||||||
configured={configuredIds.has(model.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="text-xs text-center pt-2" style={{ color: '#848E9C' }}>
|
<div className="text-xs text-center pt-2" style={{ color: '#848E9C' }}>
|
||||||
{t('modelConfig.modelsConfigured', language)}
|
{t('modelConfig.modelsConfigured', language)}
|
||||||
</div>
|
</div>
|
||||||
@@ -800,9 +777,7 @@ function StandardProviderConfigForm({
|
|||||||
>
|
>
|
||||||
<ExternalLink className="w-4 h-4" style={{ color: '#A78BFA' }} />
|
<ExternalLink className="w-4 h-4" style={{ color: '#A78BFA' }} />
|
||||||
<span className="text-sm font-medium" style={{ color: '#A78BFA' }}>
|
<span className="text-sm font-medium" style={{ color: '#A78BFA' }}>
|
||||||
{selectedModel.provider?.startsWith('blockrun')
|
{t('modelConfig.getApiKey', language)}
|
||||||
? t('modelConfig.getStarted', language)
|
|
||||||
: t('modelConfig.getApiKey', language)}
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
@@ -826,106 +801,61 @@ function StandardProviderConfigForm({
|
|||||||
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||||
</svg>
|
</svg>
|
||||||
{selectedModel.provider?.startsWith('blockrun')
|
{'API Key *'}
|
||||||
? t('modelConfig.walletPrivateKeyLabel', language)
|
|
||||||
: 'API Key *'}
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={apiKey}
|
value={apiKey}
|
||||||
onChange={(e) => onApiKeyChange(e.target.value)}
|
onChange={(e) => onApiKeyChange(e.target.value)}
|
||||||
placeholder={
|
placeholder={t('enterAPIKey', language)}
|
||||||
selectedModel.provider === 'blockrun-base'
|
|
||||||
? '0x... (EVM private key)'
|
|
||||||
: selectedModel.provider === 'blockrun-sol'
|
|
||||||
? 'bs58 encoded key (Solana)'
|
|
||||||
: t('enterAPIKey', language)
|
|
||||||
}
|
|
||||||
className="w-full px-4 py-3 rounded-xl"
|
className="w-full px-4 py-3 rounded-xl"
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Custom Base URL (hidden for BlockRun) */}
|
{/* Custom Base URL */}
|
||||||
{!selectedModel.provider?.startsWith('blockrun') && (
|
<div className="space-y-2">
|
||||||
<div className="space-y-2">
|
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
</svg>
|
||||||
</svg>
|
{t('customBaseURL', language)}
|
||||||
{t('customBaseURL', language)}
|
</label>
|
||||||
</label>
|
<input
|
||||||
<input
|
type="url"
|
||||||
type="url"
|
value={baseUrl}
|
||||||
value={baseUrl}
|
onChange={(e) => onBaseUrlChange(e.target.value)}
|
||||||
onChange={(e) => onBaseUrlChange(e.target.value)}
|
placeholder={t('customBaseURLPlaceholder', language)}
|
||||||
placeholder={t('customBaseURLPlaceholder', language)}
|
className="w-full px-4 py-3 rounded-xl"
|
||||||
className="w-full px-4 py-3 rounded-xl"
|
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
/>
|
||||||
/>
|
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
{t('leaveBlankForDefault', language)}
|
||||||
{t('leaveBlankForDefault', language)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Custom Model Name (hidden for BlockRun) */}
|
{/* Custom Model Name */}
|
||||||
{!selectedModel.provider?.startsWith('blockrun') && (
|
<div className="space-y-2">
|
||||||
<div className="space-y-2">
|
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
</svg>
|
||||||
</svg>
|
{t('customModelName', language)}
|
||||||
{t('customModelName', language)}
|
</label>
|
||||||
</label>
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
value={modelName}
|
||||||
value={modelName}
|
onChange={(e) => onModelNameChange(e.target.value)}
|
||||||
onChange={(e) => onModelNameChange(e.target.value)}
|
placeholder={t('customModelNamePlaceholder', language)}
|
||||||
placeholder={t('customModelNamePlaceholder', language)}
|
className="w-full px-4 py-3 rounded-xl"
|
||||||
className="w-full px-4 py-3 rounded-xl"
|
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
/>
|
||||||
/>
|
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
{t('leaveBlankForDefaultModel', language)}
|
||||||
{t('leaveBlankForDefaultModel', language)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* BlockRun Model Selector */}
|
|
||||||
{selectedModel.provider?.startsWith('blockrun') && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
|
||||||
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
{t('modelConfig.selectModelLabel', language)}
|
|
||||||
</label>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{BLOCKRUN_MODELS.map((m) => {
|
|
||||||
const isSelected = (modelName || BLOCKRUN_MODELS[0].id) === m.id
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={m.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => onModelNameChange(m.id)}
|
|
||||||
className="flex flex-col items-start px-3 py-2 rounded-xl text-left transition-all"
|
|
||||||
style={{
|
|
||||||
background: isSelected ? 'rgba(37, 99, 235, 0.2)' : '#0B0E11',
|
|
||||||
border: isSelected ? '1px solid #2563EB' : '1px solid #2B3139',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="text-xs font-semibold" style={{ color: isSelected ? '#60A5FA' : '#EAECEF' }}>
|
|
||||||
{m.name}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px]" style={{ color: '#848E9C' }}>{m.desc}</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Info Box */}
|
{/* Info Box */}
|
||||||
<div className="p-4 rounded-xl" style={{ background: 'rgba(139, 92, 246, 0.1)', border: '1px solid rgba(139, 92, 246, 0.2)' }}>
|
<div className="p-4 rounded-xl" style={{ background: 'rgba(139, 92, 246, 0.1)', border: '1px solid rgba(139, 92, 246, 0.2)' }}>
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
// Constants for AI model and provider configuration
|
// Constants for AI model and provider configuration
|
||||||
|
|
||||||
export interface BlockrunModel {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
desc: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Claw402Model {
|
export interface Claw402Model {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -41,16 +35,6 @@ export function getShortName(fullName: string): string {
|
|||||||
return parts.length > 1 ? parts[parts.length - 1] : fullName
|
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)
|
// Models available through Claw402 (x402 USDC payment protocol)
|
||||||
export const CLAW402_MODELS: Claw402Model[] = [
|
export const CLAW402_MODELS: Claw402Model[] = [
|
||||||
{ id: 'deepseek', name: 'DeepSeek V3', provider: 'DeepSeek', desc: '$0.003/call', icon: '🔥', price: 0.003 },
|
{ 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<string, AIProviderConfig> = {
|
|||||||
apiUrl: 'https://claw402.ai',
|
apiUrl: 'https://claw402.ai',
|
||||||
apiName: 'Claw402',
|
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)
|
// Helper function to get exchange display name from exchange ID (UUID)
|
||||||
|
|||||||
@@ -1224,7 +1224,6 @@ export const translations = {
|
|||||||
fundStep3: '$5-10 USDC lasts a long time (~$0.003/call)',
|
fundStep3: '$5-10 USDC lasts a long time (~$0.003/call)',
|
||||||
back: 'Back',
|
back: 'Back',
|
||||||
startTrading: 'Start Trading',
|
startTrading: 'Start Trading',
|
||||||
viaBlockrunWallet: 'Via BlockRun Wallet',
|
|
||||||
modelsConfigured: 'Models with gold badge are already configured',
|
modelsConfigured: 'Models with gold badge are already configured',
|
||||||
getStarted: 'Get Started',
|
getStarted: 'Get Started',
|
||||||
getApiKey: 'Get API Key',
|
getApiKey: 'Get API Key',
|
||||||
@@ -2513,7 +2512,6 @@ export const translations = {
|
|||||||
fundStep3: '充入 $5-10 USDC 即可使用很长时间(约 $0.003/次调用)',
|
fundStep3: '充入 $5-10 USDC 即可使用很长时间(约 $0.003/次调用)',
|
||||||
back: '返回',
|
back: '返回',
|
||||||
startTrading: '开始交易',
|
startTrading: '开始交易',
|
||||||
viaBlockrunWallet: '通过钱包支付',
|
|
||||||
modelsConfigured: '带金色标记的模型已配置',
|
modelsConfigured: '带金色标记的模型已配置',
|
||||||
getStarted: '开始使用',
|
getStarted: '开始使用',
|
||||||
getApiKey: '获取 API Key',
|
getApiKey: '获取 API Key',
|
||||||
@@ -3607,7 +3605,6 @@ export const translations = {
|
|||||||
fundStep3: '$5-10 USDC cukup untuk waktu lama (~$0.003/panggilan)',
|
fundStep3: '$5-10 USDC cukup untuk waktu lama (~$0.003/panggilan)',
|
||||||
back: 'Kembali',
|
back: 'Kembali',
|
||||||
startTrading: 'Mulai Trading',
|
startTrading: 'Mulai Trading',
|
||||||
viaBlockrunWallet: 'Via BlockRun Wallet',
|
|
||||||
modelsConfigured: 'Model dengan lencana emas sudah dikonfigurasi',
|
modelsConfigured: 'Model dengan lencana emas sudah dikonfigurasi',
|
||||||
getStarted: 'Mulai',
|
getStarted: 'Mulai',
|
||||||
getApiKey: 'Dapatkan API Key',
|
getApiKey: 'Dapatkan API Key',
|
||||||
|
|||||||
Reference in New Issue
Block a user