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(),
    }