| """ |
| 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"]) |
|
|
| |
| 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 |
|
|
|
|
| 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 |
|
|
|
|
| |
| 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: |
| |
| 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", |
| }, |
| ) |
|
|
| |
| 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}, |
| ) |
|
|
|
|
| |
| @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_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, |
| }, |
| ) |
|
|
| |
| |
| analyzer = ComplexityAnalyzer() |
| complexity = analyzer.analyze(body.fen, body.legal_moves) |
|
|
| |
| 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, |
| complexity=complexity, |
| ) |
|
|
| |
| coaching_resp = claude_coach.analyze(coaching_req) |
|
|
| |
| 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 |
|
|
|
|
| |
| @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", |
| } |
|
|
|
|
| |
| @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(), |
| } |
|
|