Spaces:
Runtime error
Runtime error
File size: 10,785 Bytes
e4d7d50 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | """
ChessEcon — Chess Analysis API Router (Nevermined-Protected)
=============================================================
Exposes POST /api/chess/analyze as a paid service endpoint using the
x402 payment protocol. Other teams' agents can:
1. Discover this endpoint via the Nevermined marketplace
2. Subscribe to the ChessEcon Coaching Plan (NVM_PLAN_ID)
3. Generate an x402 access token
4. Call this endpoint with the token in the `payment-signature` header
5. Receive chess position analysis powered by Claude Opus 4.5
Payment flow:
- No token → HTTP 402 with `payment-required` header (base64-encoded spec)
- Invalid token → HTTP 402 with error reason
- Valid token → Analysis delivered, 1 credit settled automatically
The endpoint also works WITHOUT Nevermined (NVM_API_KEY not set) for
local development and testing — payment verification is skipped.
"""
from __future__ import annotations
import base64
import logging
import os
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from backend.agents.claude_coach import claude_coach
from backend.agents.complexity import ComplexityAnalyzer
from backend.economy.nvm_payments import nvm_manager, NVM_PLAN_ID, NVM_AGENT_ID
from shared.models import CoachingRequest
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/chess", tags=["chess-analysis"])
# ── Request / Response models ──────────────────────────────────────────────────
class AnalyzeRequest(BaseModel):
"""Chess position analysis request."""
fen: str
legal_moves: List[str]
game_id: Optional[str] = "external"
agent_id: Optional[str] = "external_agent"
context: Optional[str] = None # Optional game context for richer analysis
class AnalyzeResponse(BaseModel):
"""Chess position analysis response."""
recommended_move: str
analysis: str
complexity_score: float
complexity_level: str
model_used: str
credits_used: int = 1
nvm_plan_id: Optional[str] = None
nvm_agent_id: Optional[str] = None
# ── Payment helper ─────────────────────────────────────────────────────────────
def _make_402_response(endpoint: str, http_verb: str = "POST") -> JSONResponse:
"""
Return an HTTP 402 response with the x402 payment-required header.
The header contains a base64-encoded PaymentRequired specification
that tells clients exactly how to pay for this service.
"""
payment_required = nvm_manager.build_payment_required(endpoint, http_verb)
if payment_required is None:
# NVM not configured — return plain 402
return JSONResponse(
status_code=402,
content={
"error": "Payment Required",
"message": (
"This endpoint requires a Nevermined payment token. "
f"Subscribe to plan {NVM_PLAN_ID} and include "
"the x402 access token in the 'payment-signature' header."
),
"nvm_plan_id": NVM_PLAN_ID or None,
"nvm_agent_id": NVM_AGENT_ID or None,
"docs": "https://nevermined.ai/docs/integrate/quickstart/5-minute-setup",
},
)
# Encode the payment spec per x402 spec
pr_json = payment_required.model_dump_json(by_alias=True)
pr_base64 = base64.b64encode(pr_json.encode()).decode()
return JSONResponse(
status_code=402,
content={
"error": "Payment Required",
"message": (
"Include your x402 access token in the 'payment-signature' header. "
f"Subscribe to plan: {NVM_PLAN_ID}"
),
"nvm_plan_id": NVM_PLAN_ID or None,
"nvm_agent_id": NVM_AGENT_ID or None,
"docs": "https://nevermined.ai/docs/integrate/quickstart/5-minute-setup",
},
headers={"payment-required": pr_base64},
)
# ── Main endpoint ──────────────────────────────────────────────────────────────
@router.post("/analyze", response_model=AnalyzeResponse)
async def analyze_position(request: Request, body: AnalyzeRequest):
"""
**Paid chess position analysis endpoint.**
Analyzes a chess position and returns the best move recommendation
with strategic reasoning, powered by Claude Opus 4.5.
**Payment:**
- Requires a Nevermined x402 access token in the `payment-signature` header
- Each call costs 1 credit from your subscribed plan
- Subscribe at: https://nevermined.app/en/subscription/{NVM_PLAN_ID}
**Without payment (NVM not configured):**
- Falls back to heuristic analysis (no Claude)
**Request body:**
```json
{
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
"legal_moves": ["e7e5", "d7d5", "g8f6", ...],
"game_id": "game_001",
"agent_id": "my_agent"
}
```
**Headers:**
- `payment-signature`: x402 access token (required when NVM is active)
**Response:**
```json
{
"recommended_move": "e7e5",
"analysis": "The move e7e5 controls the center...",
"complexity_score": 0.42,
"complexity_level": "moderate",
"model_used": "claude-opus-4-5",
"credits_used": 1
}
```
"""
endpoint_url = str(request.url)
http_verb = request.method
# ── x402 Payment Verification ──────────────────────────────────────────────
x402_token = request.headers.get("payment-signature")
if nvm_manager.available:
if not x402_token:
logger.info(
f"No payment-signature header for /api/chess/analyze "
f"from {request.client.host if request.client else 'unknown'}"
)
return _make_402_response(endpoint_url, http_verb)
is_valid, reason = nvm_manager.verify_token(
x402_token=x402_token,
endpoint=endpoint_url,
http_verb=http_verb,
max_credits="1",
)
if not is_valid:
logger.warning(f"Payment verification failed: {reason}")
return JSONResponse(
status_code=402,
content={
"error": "Payment Verification Failed",
"reason": reason,
"nvm_plan_id": NVM_PLAN_ID or None,
},
)
# ── Chess Analysis ─────────────────────────────────────────────────────────
# Assess position complexity
analyzer = ComplexityAnalyzer()
complexity = analyzer.analyze(body.fen, body.legal_moves)
# Build coaching request
coaching_req = CoachingRequest(
game_id=body.game_id or "external",
agent_id=body.agent_id or "external_agent",
fen=body.fen,
legal_moves=body.legal_moves,
wallet_balance=0.0, # External agents don't use internal wallet
complexity=complexity,
)
# Get analysis from Claude (or fallback)
coaching_resp = claude_coach.analyze(coaching_req)
# ── Settle Credits ─────────────────────────────────────────────────────────
if nvm_manager.available and x402_token:
nvm_manager.settle_token(
x402_token=x402_token,
endpoint=endpoint_url,
http_verb=http_verb,
max_credits="1",
)
response_data = AnalyzeResponse(
recommended_move=coaching_resp.recommended_move,
analysis=coaching_resp.analysis,
complexity_score=complexity.score,
complexity_level=complexity.level.value,
model_used=coaching_resp.model_used,
credits_used=1,
nvm_plan_id=NVM_PLAN_ID or None,
nvm_agent_id=NVM_AGENT_ID or None,
)
logger.info(
f"Chess analysis served: game={body.game_id} "
f"agent={body.agent_id} move={coaching_resp.recommended_move} "
f"model={coaching_resp.model_used} "
f"nvm={'settled' if (nvm_manager.available and x402_token) else 'skipped'}"
)
return response_data
# ── Service info endpoint (public, no payment required) ────────────────────────
@router.get("/service-info")
async def service_info():
"""
Public endpoint returning ChessEcon service information.
Other agents can call this to discover how to subscribe and pay.
"""
return {
"service": "ChessEcon Chess Analysis",
"description": (
"Premium chess position analysis powered by Claude Opus 4.5. "
"Subscribe to get best-move recommendations and strategic coaching."
),
"endpoint": "/api/chess/analyze",
"method": "POST",
"payment": {
"protocol": "x402",
"nvm_plan_id": NVM_PLAN_ID or "not configured",
"nvm_agent_id": NVM_AGENT_ID or "not configured",
"credits_per_request": 1,
"marketplace_url": (
f"https://nevermined.app/en/subscription/{NVM_PLAN_ID}"
if NVM_PLAN_ID else "not configured"
),
"how_to_subscribe": [
"1. Get NVM API key at https://nevermined.app",
"2. Call payments.plans.order_plan(NVM_PLAN_ID)",
"3. Call payments.x402.get_x402_access_token(NVM_PLAN_ID, NVM_AGENT_ID)",
"4. Include token in 'payment-signature' header",
],
},
"nvm_available": nvm_manager.available,
"claude_available": claude_coach.available,
"docs": "https://nevermined.ai/docs/integrate/quickstart/5-minute-setup",
}
# ── NVM transaction history (for dashboard) ────────────────────────────────────
@router.get("/nvm-transactions")
async def get_nvm_transactions(limit: int = 50):
"""Return recent Nevermined payment transactions for dashboard display."""
return {
"transactions": nvm_manager.get_transactions(limit=limit),
"nvm_status": nvm_manager.get_status(),
}
|