{
  "openapi": "3.1.0",
  "info": {
    "title": "ChainChoice API",
    "version": "1.0.0",
    "summary": "The auditable decisioning layer — as an API.",
    "description": "One POST returns a ranked recommendation, the alternatives considered, and a signed audit trail your users (or your regulator) can inspect. Same engine that powers chainchoice.io. Endpoints documented here are the v1 contract; breaking changes go to v2 with an overlap window.",
    "contact": {
      "name": "ChainChoice Partnerships",
      "email": "partnerships@chainchoice.io",
      "url": "https://chainchoice.io/docs"
    },
    "license": {
      "name": "Commercial — partner-issued API keys",
      "url": "https://chainchoice.io/docs#pricing"
    },
    "termsOfService": "https://chainchoice.io/terms"
  },
  "servers": [
    { "url": "https://chainchoice.io", "description": "Production" },
    { "url": "https://chainchoice.vercel.app", "description": "Canary / pre-prod" }
  ],
  "tags": [
    { "name": "Recommendations", "description": "Synchronous decisioning. One request, one recommendation, one receipt." },
    { "name": "Receipts", "description": "Public-by-default audit artifacts. Every decision produces one." },
    { "name": "Compliance", "description": "GDPR Article 22 + right-to-erasure surfaces." },
    { "name": "Partners", "description": "Outcome confirmation webhooks." }
  ],
  "security": [{ "BearerAuth": [] }],
  "paths": {
    "/api/v1/recommend": {
      "post": {
        "tags": ["Recommendations"],
        "summary": "Get a ranked recommendation",
        "operationId": "recommend",
        "description": "Synchronous recommendation. Returns the top pick, ranked alternatives, an effective-cost percentage, and a receipt URL. Designed for embedded fintech surfaces — wallet UIs, tax flows, treasury dashboards.",
        "security": [{ "BearerAuth": ["recommend"] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RecommendRequest" },
              "examples": {
                "exchangeBuy": {
                  "summary": "Buy $50k ETH on a US exchange",
                  "value": {
                    "query": "Best exchange for $50k ETH, US-based, security-first",
                    "category": "exchange",
                    "intent": "buy",
                    "asset": "ETH",
                    "amount_usd": 50000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recommendation served.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RecommendResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/receipts/{id}": {
      "get": {
        "tags": ["Receipts"],
        "summary": "Fetch a decision receipt (JSON)",
        "operationId": "getReceipt",
        "description": "The canonical machine-readable receipt for a decision. Mirror of public/methodology/spec/receipt.schema.json. Receipts never expire.",
        "parameters": [{ "$ref": "#/components/parameters/ReceiptId" }],
        "responses": {
          "200": {
            "description": "Receipt body.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Receipt" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/v1/receipts/{id}/explanation": {
      "get": {
        "tags": ["Compliance"],
        "summary": "GDPR Article 22 — meaningful information about the logic involved",
        "operationId": "getReceiptExplanation",
        "description": "Every recommendation served is an automated decision; EU users have a statutory right to meaningful information about the logic involved. This endpoint returns that information as JSON-LD, suitable for embedded-partner ingestion or auditor export.",
        "parameters": [{ "$ref": "#/components/parameters/ReceiptId" }],
        "responses": {
          "200": {
            "description": "Explanation document (JSON-LD).",
            "content": {
              "application/ld+json": {
                "schema": { "$ref": "#/components/schemas/Explanation" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/v1/user/{sessionId}": {
      "delete": {
        "tags": ["Compliance"],
        "summary": "Right-to-erasure",
        "operationId": "deleteUserData",
        "description": "Cascades the session's data across seven session-keyed tables (funnel events, A/B events, user results, affiliate clicks, saved decisions, outcome follow-ups, agent recommendations). Audit-logged automatically. 30-day SLA per GDPR; in practice the round trip completes in a few seconds.",
        "parameters": [
          {
            "name": "sessionId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Anonymized session identifier from the analytics session-id cookie."
          }
        ],
        "responses": {
          "200": {
            "description": "Deletion receipt.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DeletionReceipt" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/v1/admin/audit": {
      "get": {
        "tags": ["Compliance"],
        "summary": "Read the audit-log trail",
        "operationId": "readAuditLog",
        "description": "Admin/auditor surface. Returns audit_log rows newest-first within a window (default 7d, max 30d). Filters: action, target_table, actor_id. Paginate with before_id cursor. ip_hash is SHA-256, never raw. Gated by STATS_TOKEN.",
        "security": [{ "StatsTokenAuth": [] }],
        "parameters": [
          { "name": "hours", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 720 } },
          { "name": "action", "in": "query", "schema": { "type": "string", "enum": ["insert", "update", "delete", "auth_failed", "auth_succeeded", "data_export", "data_deletion"] } },
          { "name": "target_table", "in": "query", "schema": { "type": "string" } },
          { "name": "actor_id", "in": "query", "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 1000 } },
          { "name": "before_id", "in": "query", "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": {
            "description": "Audit rows + pagination cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "integer" },
                          "actor_id": { "type": ["string", "null"] },
                          "actor_role": { "type": "string" },
                          "action": { "type": "string" },
                          "target_table": { "type": "string" },
                          "target_id": { "type": ["string", "null"] },
                          "before_state": { "type": ["object", "null"] },
                          "after_state": { "type": ["object", "null"] },
                          "ip_hash": { "type": ["string", "null"] },
                          "occurred_at": { "type": "string", "format": "date-time" }
                        }
                      }
                    },
                    "next_cursor": { "type": ["integer", "null"] },
                    "window_hours": { "type": "integer" },
                    "since": { "type": "string", "format": "date-time" },
                    "count": { "type": "integer" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/partner-outcome": {
      "post": {
        "tags": ["Partners"],
        "summary": "Outbound conversion confirmation",
        "operationId": "partnerOutcome",
        "description": "Affiliate partners post here when a referred user hits a milestone (signup, KYC, first deposit, first trade, churn). The endpoint patches the originating recommendation's outcome by joining click_ref → decision_id. Idempotency is the partner's responsibility (we accept retries).",
        "security": [{ "PartnerBearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PartnerOutcomeRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmation logged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "const": true },
                    "queued": { "type": "boolean", "description": "True if write fell into the DLQ; will replay automatically." }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "503": { "description": "Partner endpoint not configured (no PARTNER_TOKENS set)." }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "cck_live_<32-hex>",
        "description": "Partner-scoped API key. SHA-256 + pepper hashed at rest; revocation is immediate. Issued per partner with scopes (`recommend`) and a per-minute rate limit."
      },
      "PartnerBearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Affiliate-partner token. Token-to-partner-name binding is enforced — a leaked token cannot insert confirmations attributed to another partner."
      },
      "StatsTokenAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Admin/auditor token (STATS_TOKEN env). Gates the funnel and audit-log read surfaces."
      }
    },
    "parameters": {
      "ReceiptId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "string", "format": "uuid" },
        "description": "Receipt UUID from a prior /api/v1/recommend response."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Request shape failed validation.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid Bearer token.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "NotFound": {
        "description": "Resource does not exist or is not visible to this token.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "RateLimited": {
        "description": "Per-minute rate limit exceeded.",
        "headers": {
          "Retry-After": { "schema": { "type": "integer" }, "description": "Seconds to wait before retrying." }
        },
        "content": {
          "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "description": "Stable machine-readable error code." },
          "message": { "type": "string" },
          "request_id": { "type": "string" }
        }
      },
      "RecommendRequest": {
        "type": "object",
        "required": ["query"],
        "properties": {
          "query": { "type": "string", "minLength": 1, "maxLength": 4000 },
          "category": {
            "type": "string",
            "enum": ["exchange", "staking", "cex_earn", "defi_lending", "self_custody", "bridge", "onramp", "tax", "card", "wallet", "yield_aggregator", "derivatives"]
          },
          "intent": {
            "type": "string",
            "enum": ["buy", "stake", "earn", "store", "send", "spend", "bridge", "compare"]
          },
          "asset": { "type": "string" },
          "amount_usd": { "type": "number", "minimum": 0 },
          "mode": { "type": "string", "enum": ["recommend", "compare"], "default": "recommend" },
          "region": { "type": "string", "description": "ISO 3166 alpha-2 or 'eu'. Auto-detected from IP if omitted." }
        }
      },
      "RecommendResponse": {
        "type": "object",
        "required": ["id", "recommended_provider", "alternatives", "receipt_url", "explanation_url"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "query": { "type": "string" },
          "recommended_provider": {
            "type": "object",
            "required": ["id", "name"],
            "properties": {
              "id": { "type": "string" },
              "name": { "type": "string" },
              "effective_cost_pct": { "type": "number" }
            }
          },
          "alternatives": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["id", "name", "rank"],
              "properties": {
                "id": { "type": "string" },
                "name": { "type": "string" },
                "rank": { "type": "integer", "minimum": 1 }
              }
            }
          },
          "rank_objective": { "type": "string" },
          "receipt_url": { "type": "string", "format": "uri" },
          "explanation_url": { "type": "string", "format": "uri" }
        }
      },
      "Receipt": {
        "$ref": "https://chainchoice.io/methodology/spec/receipt.schema.json"
      },
      "Explanation": {
        "type": "object",
        "required": ["@context", "@type", "decision_id", "logic", "human_intervention_contact"],
        "properties": {
          "@context": { "type": "string", "const": "https://schema.org" },
          "@type": { "type": "string", "const": "AutomatedDecision" },
          "decision_id": { "type": "string", "format": "uuid" },
          "logic": {
            "type": "object",
            "properties": {
              "methodology_url": { "type": "string", "format": "uri" },
              "rank_objective": { "type": "string" },
              "factors_considered": { "type": "array", "items": { "type": "string" } }
            }
          },
          "alternatives_considered": { "type": "array", "items": { "type": "object" } },
          "human_intervention_contact": { "type": "string", "format": "email" },
          "content_hash": { "type": "string", "pattern": "^[a-f0-9]{64}$", "description": "SHA-256 of canonical-JSON receipt body; mirror of recommendation_receipts.content_hash." }
        }
      },
      "DeletionReceipt": {
        "type": "object",
        "required": ["session_id", "deleted_at", "tables_affected"],
        "properties": {
          "session_id": { "type": "string" },
          "deleted_at": { "type": "string", "format": "date-time" },
          "tables_affected": {
            "type": "array",
            "items": { "type": "string" }
          },
          "rows_deleted": {
            "type": "object",
            "additionalProperties": { "type": "integer", "minimum": 0 }
          }
        }
      },
      "PartnerOutcomeRequest": {
        "type": "object",
        "required": ["partnerName", "outcome", "occurredAt"],
        "properties": {
          "partnerName": { "type": "string", "description": "Must match the partner key whose token authenticated the request." },
          "outcome": {
            "type": "string",
            "enum": ["signup", "kyc_completed", "first_deposit", "first_trade", "churned"]
          },
          "occurredAt": { "type": "string", "format": "date-time" },
          "clickRef": { "type": "string", "description": "Our session_id from the original UTM. When present, drives outcome attribution patch." },
          "partnerProductId": { "type": "string" },
          "amountUsd": { "type": "number" },
          "commissionUsd": { "type": "number" }
        }
      }
    }
  }
}
