openapi: 3.1.0
info:
  title: TILLA Data API
  version: "1.0.0"
  summary: The aligned crypto + macro feature matrix — 1,817 columns × 1-minute rows.
  description: |
    TILLA is one pre-joined dataset: **1,817 columns × 1-minute rows, July 2023 → now**,
    every field on a single timestamp. Crypto OHLCV + indicators, four-venue derivatives,
    on-chain (incl. BTC node microstructure), options, per-fund ETF flows, FX (29 pairs),
    equities & miners, and full macro. Built for quants, ML builders and AI agents.

    **Auth:** send `Authorization: Bearer tk_live_...`. Get a free key at
    https://tillacrypto.com (free tier: 31 fields, 1-week history, no card).

    **Tiers** (cumulative, most-valuable-first): Free 31 · Trader 157 · Builder 617 ·
    Pro 1,264 · Max 1,817 · Enterprise all 1,817 (uncapped + priority). Commercial use
    requires Pro/Max; resale/redistribution is available only under an Enterprise license.

    There is also an MCP server (`tilla-mcp`) so agents can call this API directly.
  contact:
    name: TILLA
    email: support@tillacrypto.com
    url: https://tillacrypto.com
  license:
    name: TILLA data license (commercial use Pro/Max; resale only under Enterprise)
    url: https://tillacrypto.com/#docs
servers:
  - url: https://api.tillacrypto.com
    description: Production
  - url: http://127.0.0.1:8898
    description: Local development
externalDocs:
  description: Docs & field dictionary
  url: https://tillacrypto.com/#docs
security:
  - BearerKey: []
tags:
  - name: Data
    description: The feature matrix.
  - name: Meta
    description: Keyless status, freshness and plan catalog.
paths:
  /v1/status:
    get:
      tags: [Meta]
      operationId: getStatus
      summary: Live freshness (no key)
      description: Feed up/down, latest row timestamp, and each tier's window + row count. Cached ~30s.
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ts: { type: string, format: date-time }
                  feed_up: { type: boolean }
                  latest_row: { type: string, nullable: true }
                  tiers:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        window_start: { type: string, nullable: true }
                        window_end: { type: string, nullable: true }
                        rows: { type: integer }
  /v1/health:
    get:
      tags: [Meta]
      operationId: getHealth
      summary: Health & feed freshness (no key)
      security: []
      responses:
        "200": { description: OK }
  /v1/tiers:
    get:
      tags: [Meta]
      operationId: getTiers
      summary: Plan catalog (no key)
      description: History depth, requests/min, and daily quota per tier.
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: object
                  properties:
                    history_days: { type: integer, nullable: true }
                    per_minute: { type: integer }
                    daily_quota: { type: integer }
              example:
                free: { history_days: 7, per_minute: 10, daily_quota: 500 }
                trader: { history_days: 183, per_minute: 60, daily_quota: 50000 }
                builder: { history_days: 397, per_minute: 120, daily_quota: 200000 }
                pro: { history_days: 1096, per_minute: null, daily_quota: null }
                max: { history_days: 1096, per_minute: null, daily_quota: null }
                enterprise: { history_days: 1096, per_minute: null, daily_quota: null }
  /v1/schema:
    get:
      tags: [Data]
      operationId: getSchema
      summary: The columns your tier serves
      description: Every column your key can pull, plus the lowest tier carrying each. Call this before requesting data — never guess column names.
      responses:
        "200": { description: OK }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "503": { $ref: "#/components/responses/TierUnavailable" }
  /v1/features:
    get:
      tags: [Data]
      operationId: getFeatures
      summary: Dataset summary (count + window)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  tier: { type: string }
                  count: { type: integer }
                  window_start: { type: string, nullable: true }
                  window_end: { type: string, nullable: true }
                  columns: { type: array, items: { type: string } }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /v1/latest:
    get:
      tags: [Data]
      operationId: getLatest
      summary: Newest 1-minute row (same-second)
      parameters:
        - $ref: "#/components/parameters/Cols"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  timestamp: { type: string }
                  source: { type: string, enum: [memory, db] }
                  data: { type: object, additionalProperties: true }
              example:
                timestamp: "2026-07-12 01:23:00"
                source: memory
                data: { btc_close: 64200.0, fear_greed: 22 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/TierForbidden" }
  /v1/data:
    get:
      tags: [Data]
      operationId: getData
      summary: A date range of rows
      description: Returns up to `limit` rows in `[start, end]` at `freq`. Use the `next_start` cursor to page forward. `format=csv` streams CSV with an `X-Next-Start` header.
      parameters:
        - $ref: "#/components/parameters/Cols"
        - name: start
          in: query
          schema: { type: string }
          description: "Window floor, 'YYYY-MM-DD[ HH:MM[:SS]]' (UTC)."
        - name: end
          in: query
          schema: { type: string }
          description: "Window ceiling (same format). Optional."
        - name: freq
          in: query
          schema: { type: string, enum: [1m, 5m, 15m, 1h, 4h, 1d], default: 1m }
        - name: limit
          in: query
          schema: { type: integer, default: 1440, maximum: 5000 }
          description: "Rows per page. Capped at 5000 (2000 for coarse freqs)."
        - $ref: "#/components/parameters/Format"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  rows: { type: integer }
                  freq: { type: string }
                  columns: { type: array, items: { type: string } }
                  truncated: { type: boolean }
                  next_start: { type: string, nullable: true }
                  data:
                    type: array
                    items: { type: array, items: {} }
            text/csv:
              schema: { type: string }
        "400": { $ref: "#/components/responses/BadParam" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/TierForbidden" }
  /v1/sync:
    get:
      tags: [Data]
      operationId: getSync
      summary: Everything since a timestamp (incremental)
      description: |
        The incremental endpoint — the fast way to build and refresh a local copy.
        Send the newest timestamp you have as `since`; get every row after it. Start
        empty with `since=1970-01-01`, then keep passing back `next_since` until
        `complete` is true. CSV sets `X-Next-Since` and `X-Rows` headers.
      parameters:
        - name: since
          in: query
          required: true
          schema: { type: string }
          description: "'YYYY-MM-DD[ HH:MM[:SS]]'. Use 1970-01-01 to pull the whole window."
        - $ref: "#/components/parameters/Cols"
        - name: limit
          in: query
          schema: { type: integer, default: 5000, maximum: 5000 }
        - $ref: "#/components/parameters/Format"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  rows: { type: integer }
                  next_since: { type: string }
                  complete: { type: boolean }
                  columns: { type: array, items: { type: string } }
                  data:
                    type: array
                    items: { type: array, items: {} }
        "400": { $ref: "#/components/responses/BadParam" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/TierForbidden" }
  /v1/usage:
    get:
      tags: [Data]
      operationId: getUsage
      summary: Your account status & limits
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  tier: { type: string }
                  name: { type: string }
                  customer_no: { type: integer, nullable: true }
                  today: { type: integer }
                  columns: { type: integer }
                  history_days: { type: integer }
                  calls_total: { type: integer }
                  per_minute: { type: integer }
                  daily_quota: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }
components:
  securitySchemes:
    BearerKey:
      type: http
      scheme: bearer
      bearerFormat: tk_live
      description: "Your TILLA API key. Header: `Authorization: Bearer tk_live_...`"
  parameters:
    Cols:
      name: cols
      in: query
      required: false
      schema: { type: string }
      description: "Comma-separated subset of columns (see /v1/schema). Omit for all columns your tier serves."
    Format:
      name: format
      in: query
      required: false
      schema: { type: string, enum: [json, csv], default: json }
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "missing API key — Authorization: Bearer tk_live_..." }
    TierForbidden:
      description: The requested data is outside your plan.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    BadParam:
      description: Bad parameter.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    TierUnavailable:
      description: This tier's data is temporarily unavailable (fails closed).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Error:
      type: object
      properties:
        error: { type: string }
