{
  "openapi": "3.1.0",
  "info": {
    "title": "Skontro Public API",
    "version": "1.0.0",
    "summary": "Measured all-in crypto trading cost + sourced exchange reliability — no key required.",
    "description": "The same numbers that power skontro.com, as a public read-only REST API. No API key, no account. Every live response carries a `disclaimer` string and per-venue data-age fields (seconds); missing data is shown as absent, never guessed. Please cite \"Skontro (skontro.com)\" and include the data age when quoting figures — these are live measurements, not static tables.\n\nNot investment advice. Skontro is not a CASP under MiCAR — we show market data, you decide. Ranking is always by net amount received and is never influenced by affiliate payout.\n\nRate limits: 60 requests/min per IP on each endpoint (20/min on /api/truecost/swap, a real DEX round-trip), under a global 120/min per-IP ceiling. No SLA — best effort. Breaking changes are announced at least 14 days ahead on /developers and /status. There is an MCP server over the same engine at https://cryptotraderapi-production.up.railway.app/api/mcp (see /developers).",
    "contact": {
      "name": "Skontro",
      "url": "https://skontro.com/developers"
    }
  },
  "servers": [
    {
      "url": "https://cryptotraderapi-production.up.railway.app",
      "description": "Skontro public API"
    }
  ],
  "tags": [
    {
      "name": "true-cost",
      "description": "Measured all-in trading cost."
    },
    {
      "name": "reliability",
      "description": "Sourced Reliability Score per exchange."
    },
    {
      "name": "ops",
      "description": "Feed freshness and service health."
    }
  ],
  "paths": {
    "/api/truecost": {
      "get": {
        "tags": [
          "true-cost"
        ],
        "summary": "Ranked all-in cost of one trade across all covered exchanges.",
        "description": "Published fee + hidden spread (from walking the live order book for your exact size), optionally + withdrawal fee, ranked by net units received (buying: coins; selling: euros). Ranking is by net amount only — never fee headline, never affiliate payout. Every live response carries a `disclaimer` string and per-venue data-age fields (seconds); missing data is shown as absent, never guessed.",
        "parameters": [
          {
            "name": "asset",
            "in": "query",
            "required": true,
            "description": "Ticker, e.g. BTC. Supported assets are enumerated by /api/truecost/venues.",
            "schema": {
              "type": "string",
              "example": "BTC"
            }
          },
          {
            "name": "amount",
            "in": "query",
            "required": true,
            "description": "Trade size — EUR when side=buy, asset units when side=sell.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "exclusiveMinimum": true,
              "example": 1000
            }
          },
          {
            "name": "side",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "buy",
                "sell"
              ],
              "default": "buy"
            }
          },
          {
            "name": "quote",
            "in": "query",
            "required": false,
            "description": "Quote currency for the order book. EUR is the default and the tested path.",
            "schema": {
              "type": "string",
              "default": "EUR",
              "example": "EUR"
            }
          },
          {
            "name": "includeWithdrawal",
            "in": "query",
            "required": false,
            "description": "Include the curated native-chain withdrawal fee in each total. Venues with no published fixed fee keep their total withdrawal-free and are flagged.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked comparison for the requested trade.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrueCostResponse"
                }
              }
            }
          },
          "400": {
            "description": "Unsupported asset (see /api/truecost/venues).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Live data backend temporarily unavailable (Retry-After header set).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/truecost/venues": {
      "get": {
        "tags": [
          "true-cost"
        ],
        "summary": "Covered exchanges with fee schedule + freshness metadata.",
        "description": "The list of covered venues, each with its base-tier maker/taker fee (bps), whether it has a public order book, whether the venue link is monetized, the fee source and the last fee-verification date. Enumerates the supported venue set (no hardcoded count in copy — this endpoint is the source of truth).",
        "responses": {
          "200": {
            "description": "Array of venue metadata rows.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/VenueRow"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/truecost/withdrawal-fees": {
      "get": {
        "tags": [
          "true-cost"
        ],
        "summary": "Curated native-chain BTC/ETH withdrawal fees per exchange, EUR-valued.",
        "description": "Curated withdrawal fees for BTC and ETH on their native chains, each with its official source URL and EUR value at the cached reference price. Venues without a published fixed fee return null rows — absent means unknown, never zero.",
        "responses": {
          "200": {
            "description": "Per-venue withdrawal-fee map plus the reference prices used.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WithdrawalFeesResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/truecost/index": {
      "get": {
        "tags": [
          "true-cost"
        ],
        "summary": "Monthly True-Cost-Index — measured cost per €1,000 trade per exchange.",
        "description": "The monthly aggregate (average fee, hidden spread and all-in cost per €1,000 trade per exchange) from hourly order-book measurements. `report` is null until a month has accrued enough data — the placeholder shape still returns `availableMonths` and `generatedAt` (an honest \"first report soon\" state, never a fabricated table).",
        "parameters": [
          {
            "name": "month",
            "in": "query",
            "required": false,
            "description": "YYYY-MM. Omitted → the latest month that has rankable data.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}$",
              "example": "2026-06"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The requested (or latest) month. `report` may be null with a populated `availableMonths` when no full month has accrued yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IndexResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/truecost/swap": {
      "get": {
        "tags": [
          "true-cost"
        ],
        "summary": "DEX swap route (LiFi-aggregated) in the same cost shape as the CEX table.",
        "description": "One LiFi-aggregated route per pair (ETH, WBTC, USDC, USDT, DAI on Ethereum), EUR-framed onto the same cost model. When no route or reference is available it returns an honest 503 rather than a guessed number — a documented A1.9 behaviour, not an error to retry blindly. Tighter 20/min limit: a cache miss is a real LiFi round-trip.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "ETH",
                "WBTC",
                "USDC",
                "USDT",
                "DAI"
              ],
              "example": "ETH"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "ETH",
                "WBTC",
                "USDC",
                "USDT",
                "DAI"
              ],
              "example": "USDC"
            }
          },
          {
            "name": "amount",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": 0,
              "exclusiveMinimum": true,
              "example": 1
            }
          },
          {
            "name": "chain",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "ETHEREUM",
              "example": "ETHEREUM"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The DEX route quote (one venue row, LiFi-labelled).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SwapResponse"
                }
              }
            }
          },
          "400": {
            "description": "Unsupported token, or from === to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "No DEX route or reference price available right now — the honest \"we show nothing rather than a guess\" state (A1.9). The body is the Error shape, NOT a zeroed quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/trust": {
      "get": {
        "tags": [
          "reliability"
        ],
        "summary": "Sourced Reliability Score per exchange (0–100, six weighted components).",
        "description": "The Reliability Score for every covered exchange — six weighted components (withdrawal reliability, proof of reserves, regulatory standing, security history, support, maturity), every negative datapoint dated and sourced. Detail and history per exchange: GET /api/trust/{exchange} and GET /api/trust/{exchange}/history (404-safe for unknown slugs). Describes recorded facts — not a recommendation to use or avoid any venue.",
        "responses": {
          "200": {
            "description": "Methodology version, generation time, and all scored exchanges.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrustListResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/status": {
      "get": {
        "tags": [
          "ops"
        ],
        "summary": "Per-venue feed freshness + backend health, live.",
        "description": "The public data-status snapshot: per-venue order-book age for the BTC/ETH anchors, active accuracy suppressions, estimated venues, worker job heartbeats and the overall backend state. This is where our own feed degradation is public.",
        "responses": {
          "200": {
            "description": "The live status snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResponse"
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "ops"
        ],
        "summary": "Liveness probe — db + redis reachability.",
        "description": "A minimal health probe: overall status plus db and redis reachability.",
        "responses": {
          "200": {
            "description": "Health snapshot (status ok/degraded).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "description": "NestJS default error envelope (also the honest 503/400 body).",
        "properties": {
          "statusCode": {
            "type": "integer",
            "example": 503
          },
          "message": {
            "type": "string",
            "example": "Live data backend is unavailable — the comparison resumes shortly."
          },
          "error": {
            "type": "string",
            "example": "Service Unavailable"
          }
        }
      },
      "TrueCostResponse": {
        "type": "object",
        "required": [
          "query",
          "refPriceEur",
          "eurUsdRate",
          "generatedAt",
          "venues",
          "ageSpreadSeconds",
          "ageWarning",
          "disclaimer"
        ],
        "properties": {
          "query": {
            "type": "object",
            "properties": {
              "asset": {
                "type": "string"
              },
              "amount": {
                "type": "number"
              },
              "quote": {
                "type": "string"
              },
              "side": {
                "type": "string",
                "enum": [
                  "buy",
                  "sell"
                ]
              },
              "includeWithdrawal": {
                "type": "boolean"
              }
            }
          },
          "refPriceEur": {
            "type": [
              "number",
              "null"
            ],
            "description": "EUR reference price; null on a cold cache."
          },
          "eurUsdRate": {
            "type": [
              "number",
              "null"
            ],
            "description": "Display-only USD toggle rate; never a ranking input."
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "venues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TrueCostVenueRow"
            }
          },
          "ageSpreadSeconds": {
            "type": "number",
            "description": "Max age difference across live snapshots (K6)."
          },
          "ageWarning": {
            "type": "boolean"
          },
          "disclaimer": {
            "type": "string"
          }
        }
      },
      "TrueCostVenueRow": {
        "type": "object",
        "description": "One ranked venue. Carries its own data age in seconds.",
        "properties": {
          "venue": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          },
          "effectiveUnits": {
            "type": "number",
            "description": "Net units received — the ranking quantity (K4)."
          },
          "totalCostEur": {
            "type": [
              "number",
              "null"
            ]
          },
          "feeEur": {
            "type": [
              "number",
              "null"
            ]
          },
          "spreadEur": {
            "type": [
              "number",
              "null"
            ]
          },
          "estimated": {
            "type": "boolean"
          },
          "isCheapest": {
            "type": "boolean"
          },
          "affiliateUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "monetized": {
            "type": "boolean",
            "description": "Disclosure flag — never a ranking input (G9)."
          },
          "dataAgeSeconds": {
            "type": [
              "number",
              "null"
            ]
          },
          "flags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "VenueRow": {
        "type": "object",
        "required": [
          "venue",
          "displayName",
          "hasPublicOrderbook",
          "monetized",
          "makerBps",
          "takerBps"
        ],
        "properties": {
          "venue": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          },
          "hasPublicOrderbook": {
            "type": "boolean"
          },
          "monetized": {
            "type": "boolean"
          },
          "makerBps": {
            "type": "number"
          },
          "takerBps": {
            "type": "number"
          },
          "feeSource": {
            "type": [
              "string",
              "null"
            ]
          },
          "feeUpdatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "WithdrawalFeesResponse": {
        "type": "object",
        "required": [
          "generatedAt",
          "refPrices",
          "venues",
          "note"
        ],
        "properties": {
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "refPrices": {
            "type": "object",
            "additionalProperties": {
              "type": [
                "number",
                "null"
              ]
            }
          },
          "venues": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "venue": {
                  "type": "string"
                },
                "displayName": {
                  "type": "string"
                },
                "fees": {
                  "type": "object",
                  "additionalProperties": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "feeAmount": {
                        "type": "number"
                      },
                      "feeAsset": {
                        "type": "string"
                      },
                      "feeEur": {
                        "type": [
                          "number",
                          "null"
                        ]
                      },
                      "source": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "note": {
            "type": "string"
          }
        }
      },
      "IndexResponse": {
        "type": "object",
        "required": [
          "report",
          "availableMonths",
          "generatedAt"
        ],
        "description": "Monthly True-Cost-Index. `report` is null (placeholder shape) until a month has accrued enough data; `availableMonths` and `generatedAt` are always present.",
        "properties": {
          "report": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "description": "Aggregate report for one month (per-venue averages, cheapest/priciest shares).",
                "additionalProperties": true
              }
            ]
          },
          "availableMonths": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}$"
            }
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SwapResponse": {
        "type": "object",
        "required": [
          "query",
          "refFromEur",
          "refToEur",
          "amountInEur",
          "generatedAt",
          "venues"
        ],
        "properties": {
          "query": {
            "type": "object",
            "properties": {
              "from": {
                "type": "string"
              },
              "to": {
                "type": "string"
              },
              "amount": {
                "type": "number"
              },
              "chain": {
                "type": "string"
              }
            }
          },
          "refFromEur": {
            "type": "number"
          },
          "refToEur": {
            "type": "number"
          },
          "amountInEur": {
            "type": "number"
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "venues": {
            "type": "array",
            "description": "A single DEX route row (LiFi-labelled), same shape as a CEX venue row.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          }
        }
      },
      "TrustListResponse": {
        "type": "object",
        "required": [
          "methodologyVersion",
          "generatedAt",
          "exchanges"
        ],
        "properties": {
          "methodologyVersion": {
            "type": "string"
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "exchanges": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "One exchange: score 0–100 plus its six sourced components.",
              "additionalProperties": true
            }
          }
        }
      },
      "StatusResponse": {
        "type": "object",
        "required": [
          "backend",
          "thresholdSeconds",
          "venues",
          "generatedAt"
        ],
        "properties": {
          "backend": {
            "type": "string",
            "enum": [
              "up",
              "down"
            ]
          },
          "thresholdSeconds": {
            "type": "number"
          },
          "venues": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "venue": {
                  "type": "string"
                },
                "displayName": {
                  "type": "string"
                },
                "ageSeconds": {
                  "type": "object",
                  "additionalProperties": {
                    "type": [
                      "number",
                      "null"
                    ]
                  }
                },
                "state": {
                  "type": "string",
                  "enum": [
                    "fresh",
                    "degraded",
                    "no_data",
                    "unknown"
                  ]
                }
              }
            }
          },
          "suppressions": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "estimatedVenues": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "jobs": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "HealthResponse": {
        "type": "object",
        "required": [
          "status",
          "db",
          "redis"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "degraded"
            ]
          },
          "db": {
            "type": "string",
            "enum": [
              "up",
              "down"
            ]
          },
          "redis": {
            "type": "string",
            "enum": [
              "up",
              "down"
            ]
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  }
}