openapi: 3.0.3
info:
  title: pigi.finance Vault Intelligence API
  version: "1.0.0"
  description: >
    Programmatic access to pigi.finance DeFi vault data: APR, APY, TVL
    (with 30-day moving averages) across daily and weekly/monthly/quarterly/yearly
    windows, plus holder counts, risk ratings and DeFi/T-Bill reference rates. Authenticate by exchanging your API key for a
    short-lived JWT, then send `Authorization: Bearer <JWT>` on every request.


    PLANS (free / pro / custom — https://pigi.finance/defi-api). Every plan gets every
    rate, TVL and window figure, the holder COUNT (`Vault.holders`) and Basic risk
    (`/vaults/{id}/risk`: band, score, history, floors). Pro adds the holder LIST
    (`/vaults/{id}/holders`, with its concentration block) and the holder-analytics
    fields on `Vault` (`deposit_count`, `active_addresses_30d`, `tvl_concentration_top10pct`).
    Custom adds the `full` risk decomposition. A gated FIELD is served as null and named
    in a top-level
    `plan_required` object (`{ "<field>": "<plan>" }`); a gated ENDPOINT answers
    403 `{ "error": "plan_required", "required": "<plan>" }`. A null with no marker is a
    real null (not indexed yet, vault not assessed). Gated elements below carry `x-plan`.
servers:
  - url: https://pigi.finance/api/v1
tags:
  - name: meta
  - name: auth
  - name: vaults
  - name: rates
  - name: hacks
  - name: account
paths:
  /:
    get:
      tags: [meta]
      summary: API discovery index
      description: Version, docs link, and the list of available endpoints. No auth.
      security: []
      responses:
        "200":
          description: Discovery document
          content:
            application/json:
              schema:
                type: object
                properties:
                  version: { type: string, example: v1 }
                  docs: { type: string, example: /api-docs/openapi.yaml }
                  auth: { type: string }
                  endpoints:
                    type: array
                    items: { type: string }
  /auth/token:
    post:
      tags: [auth]
      summary: Exchange an API key for a short-lived JWT
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [apiKey]
              properties:
                apiKey:
                  type: string
                  example: pigi_live_xxxxxxxxxxxxxxxxxxxxxxxx
      responses:
        "200":
          description: A bearer token
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }
                  expiresIn: { type: integer, description: seconds }
                  plan: { $ref: "#/components/schemas/Plan" }
        "400": { description: missing_api_key }
        "401": { description: invalid_api_key }
  /vaults:
    get:
      tags: [vaults]
      summary: List vaults
      parameters:
        - { name: protocol_name, in: query, schema: { type: string } }
        - { name: chain_id, in: query, schema: { type: string } }
        - { name: strategy_id, in: query, schema: { type: string } }
        - { name: tvl_filter, in: query, schema: { type: string, enum: [gte_1m, gte_2m, gte_5m, gte_10m, non_na, na] } }
        - { name: apr_filter, in: query, schema: { type: string, enum: [lte_10, gt_5, gt_10, gt_15, gt_20, non_na, na] } }
        - { name: age_filter, in: query, schema: { type: string, enum: [new, 3mo, 6mo, 12mo, 18mo, 24mo, non_na, na] } }
        - { name: limit, in: query, schema: { type: integer, default: 100, maximum: 1000 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: A page of vaults
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Vault" }
                  pagination:
                    type: object
                    properties:
                      total: { type: integer }
                      limit: { type: integer }
                      offset: { type: integer }
                      hasMore: { type: boolean }
                  availableFilters:
                    type: object
                    properties:
                      protocol_names: { type: array, items: { type: string } }
                      chain_ids: { type: object, additionalProperties: { type: array, items: { type: integer } } }
                      all_chain_ids: { type: array, items: { type: integer } }
                  plan_required: { $ref: "#/components/schemas/PlanRequired" }
        "401": { description: invalid_token }
  /vaults/{id}:
    get:
      tags: [vaults]
      summary: Get a single vault by pool id
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        "200":
          description: The vault
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Vault" }
                  plan_required: { $ref: "#/components/schemas/PlanRequired" }
        "404": { description: not_found }
  /vaults/{id}/history:
    get:
      tags: [vaults]
      summary: Daily time-series for a vault (by strategy id)
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer }, description: strategy id }
        - { name: range, in: query, schema: { type: string, enum: ["7D", "30D", "90D", "180D"], default: "7D" } }
      responses:
        "200":
          description: Time-series + latest values
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        timestamp: { type: string }
                        tvl: { type: number }
                        apr: { type: number }
                        apy: { type: number }
                        ra_apr: { type: number, nullable: true, description: "risk-adjusted APR: apr minus a penalty derived from the vault's published overall risk score (0-100, higher = safer; piecewise-linear); null when the vault has no published assessment" }
                        tvl_30d_ma: { type: number }
                        apr_30d_ma: { type: number }
                        apy_30d_ma: { type: number }
                  lastTvl: { type: number }
                  lastApr: { type: number }
                  lastApy: { type: number }
                  lastRaApr: { type: number, nullable: true }
                  lastTvl30dMa: { type: number }
                  lastApr30dMa: { type: number }
                  lastApy30dMa: { type: number }
                  lastRaApr30dMa: { type: number, nullable: true }
  /vaults/{id}/stats:
    get:
      tags: [vaults]
      summary: Windowed stats for a vault (by strategy id)
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer }, description: strategy id }
        - { name: period, in: query, schema: { type: string, enum: [weekly, monthly, quarterly, yearly, lifetime] } }
      responses:
        "200":
          description: Windowed stats (all windows if period omitted)
          content:
            application/json:
              schema:
                type: object
                properties:
                  weekly: { $ref: "#/components/schemas/PeriodStats" }
                  monthly: { $ref: "#/components/schemas/PeriodStats" }
                  quarterly: { $ref: "#/components/schemas/PeriodStats" }
                  yearly: { $ref: "#/components/schemas/PeriodStats" }
                  lifetime: { $ref: "#/components/schemas/PeriodStats" }
  /vaults/{id}/holders:
    get:
      tags: [vaults]
      summary: Holder list for a vault (by strategy id) — Pro plan
      x-plan: pro
      description: >
        PRO PLAN. The vault's holder list, ranked by share-token balance, with each
        address's share of the total and the cumulative share down the ranking, plus
        `meta.concentration`. A Free client receives 403 plan_required. (Vault.holders exposes the holder
        count on every plan.) The list is replayed from on-chain transfers by pigi's
        holders scan: a vault not yet scanned with per-address storage returns
        `data: []` with `meta.scan: null` — not scanned, not zero holders. `balance`
        is in raw share-token units and loses precision above 2^53; read the shares.
        Above 20,000 holders the total is not walked: `share_pct` is null and
        `meta.total_known` is false.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer }, description: strategy id }
        - { name: limit, in: query, schema: { type: integer, default: 100, maximum: 500 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: One page of the holder list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Holder" }
                  meta:
                    allOf:
                      - { $ref: "#/components/schemas/HolderListMeta" }
                      - type: object
                        properties:
                          concentration:
                            nullable: true
                            allOf: [{ $ref: "#/components/schemas/Concentration" }]
                            description: The map's concentration at the scan's block; null until the vault's next scan writes it.
        "403":
          description: plan_required (Free plan)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PlanRequiredError" }
  /vaults/{id}/risk:
    get:
      tags: [vaults]
      summary: Published risk assessment for a vault (by strategy id)
      description: >
        pigi's published risk rating. Every plan gets the Basic block — band, overall
        score (0-100, higher = safer), their date-keyed history over `range`, the band
        thresholds under the current methodology, event markers, and any binding hard
        floor (the standing warning, e.g. "redemption blocked"). CUSTOM additionally gets
        `full`: the structural rosette with its ladder rungs and precedents, the
        quantitative anchor inputs, the dependency cap (edges, min, slack, binding),
        overlays, floors with their cap arithmetic, the blend and the final computation,
        plus the per-day anchor / rosette / cap series. Below Custom, `full` is null and
        `plan_required` is `{ "full": "custom" }`. `assessed: false` means the vault has
        no published assessment (most vaults).
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer }, description: strategy id }
        - { name: range, in: query, schema: { type: string, enum: ["7D", "30D", "90D", "180D", "365D", "ALL"], default: "90D" } }
      responses:
        "200":
          description: The assessment
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VaultRisk" }
        "400": { description: invalid_id / invalid_parameter }
        "503": { description: risk_unavailable — the risk database could not be reached; retry }
  /rates:
    get:
      tags: [rates]
      summary: DeFi Base Rate (stablecoin + ETH) and T-Bill risk-free rate history
      description: >
        Daily history of the pigi.finance DeFi Base Rate — the mean yield across
        the tracked stablecoin and ETH vault sets — alongside the 3-month U.S.
        T-Bill risk-free rate. Series are sorted oldest to newest.
      responses:
        "200":
          description: Rate history series
          content:
            application/json:
              schema:
                type: object
                properties:
                  stable:
                    type: array
                    items: { $ref: "#/components/schemas/RatePoint" }
                  eth:
                    type: array
                    items: { $ref: "#/components/schemas/RatePoint" }
                  tbills:
                    type: array
                    items: { $ref: "#/components/schemas/TBillPoint" }
  /hacks:
    get:
      tags: [hacks]
      summary: DeFi and TradFi hack / loss events
      description: >
        Hack and loss events: every tracked DeFi exploit plus a reference set of
        major TradFi banking losses. `summary` covers the entire filtered set (not
        just the returned page), so total losses for a window can be read without
        paging through every event.
      parameters:
        - in: query
          name: category
          schema: { type: string, enum: [defi, tradfi, all], default: all }
        - in: query
          name: type
          description: >
            What kind of protocol was hit. Case-insensitive. DeFi events only —
            TradFi rows carry no type, so this filter excludes them.
          schema: { type: string, enum: [DeFi, Dexes, Bridges, all], default: all }
        - in: query
          name: from
          description: Only events on/after this date (YYYY-MM-DD or ISO 8601).
          schema: { type: string }
        - in: query
          name: to
          description: Only events on/before this date, inclusive of the whole day.
          schema: { type: string }
        - in: query
          name: min_amount
          description: Only events at or above this USD amount.
          schema: { type: number }
        - in: query
          name: sort
          schema:
            type: string
            enum: [date_asc, date_desc, amount_desc, amount_asc]
            default: date_asc
        - in: query
          name: limit
          schema: { type: integer, default: 100, maximum: 1000 }
        - in: query
          name: offset
          schema: { type: integer, default: 0 }
      responses:
        "200":
          description: Hack events, a summary of the filtered set, and pagination
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Hack" }
                  summary:
                    type: object
                    description: Totals over the ENTIRE filtered set, not just the returned page.
                    properties:
                      count: { type: integer }
                      total_amount_hacked: { type: number, description: "Sum of amount_hacked (USD)." }
                      first_date: { type: string, nullable: true }
                      last_date: { type: string, nullable: true }
                  pagination:
                    type: object
                    properties:
                      total: { type: integer }
                      limit: { type: integer }
                      offset: { type: integer }
                      hasMore: { type: boolean }
        "400":
          description: Invalid query parameter
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string, example: invalid_parameter }
                  detail: { type: string }
  /usage:
    get:
      tags: [account]
      summary: Your request usage for the current period
      responses:
        "200":
          description: Usage counters
          content:
            application/json:
              schema:
                type: object
                properties:
                  clientId: { type: integer }
                  plan: { $ref: "#/components/schemas/Plan" }
                  period:
                    type: object
                    properties:
                      start: { type: string }
                      end: { type: string }
                  used: { type: integer }
                  limit: { type: integer, nullable: true }
                  remaining: { type: integer, nullable: true }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  schemas:
    Plan:
      type: string
      enum: [free, pro, custom]
      description: The caller's plan. Unknown values in the account record read as free.
    PlanRequired:
      type: object
      description: >
        Present only when the caller's plan withholds fields of this response: each key is
        a field that came back null for that reason, its value the plan that unlocks it.
      additionalProperties: { $ref: "#/components/schemas/Plan" }
      example: { deposit_count: pro, active_addresses_30d: pro, tvl_concentration_top10pct: pro }
    PlanRequiredError:
      type: object
      properties:
        error: { type: string, enum: [plan_required] }
        required: { $ref: "#/components/schemas/Plan" }
    Vault:
      type: object
      properties:
        id: { type: integer }
        strategy_id: { type: integer }
        protocol_name: { type: string }
        chain_id: { type: integer }
        pool_name: { type: string }
        pool_address: { type: string }
        asset_address: { type: string, nullable: true }
        type: { type: string }
        tvl_30d_ma: { type: number, nullable: true }
        apr_30d_ma: { type: number, nullable: true }
        display_name: { type: string, nullable: true, description: "pool_name plus the minimum qualifier (chain · symbol · framework) that makes it unique. Prefer this for display; null until the row's first nightly." }
        holders: { type: integer, nullable: true, description: "Holder count. Every plan." }
        pool_creation_date: { type: string, nullable: true }
        updated_at: { type: string, nullable: true }
        tvl_flow_1d: { type: number, nullable: true }
        tvl_flow_7d: { type: number, nullable: true }
        apr_trend_1d: { type: number, nullable: true }
        risk_band: { type: string, nullable: true, description: "Published pigi-risk band: A (safest) .. F. null when the vault has not been assessed yet." }
        risk_score: { type: number, nullable: true, description: "Published overall risk score, 0-100 (higher = safer). null when unassessed." }
        active_addresses_30d: { type: integer, nullable: true, x-plan: pro, description: "PRO. Distinct depositors in the trailing 30 days ending at the last scan: owner addresses on ERC-4626 Deposit logs (Morpho, Euler, Yearn); for Uniswap V3 the current owners of positions minted inside the window. null + plan_required on Free; null with no marker until the vault has been scanned (Aave reserves are not scanned)." }
        deposit_count: { type: integer, nullable: true, x-plan: pro, description: "PRO. Deposit events over the vault's indexed life: ERC-4626 Deposit logs (Morpho, Euler, Yearn, generic 4626), pool Mint logs for Uniswap V3; withdrawals are not counted. null + plan_required on Free; null with no marker until the vault has been scanned with per-address storage (Aave reserves are not scanned)." }
        tvl_concentration_top10pct: { type: number, nullable: true, x-plan: pro, description: "PRO. % of TVL held by the top 10% of holders. null + plan_required on Free; null with no marker while holder analytics are still being indexed." }
    PeriodStats:
      type: object
      description: Aggregated risk/return stats for a single time window.
      properties:
        tvl_low: { type: number, nullable: true }
        tvl_high: { type: number, nullable: true }
        apr: { type: number, nullable: true }
        apy: { type: number, nullable: true }
        inflows: { type: number, nullable: true, description: "Net TVL change over the window (last day − first day): positive = net inflow, negative = net outflow. TVL-based, so it also reflects yield earned over the window, not just deposits/withdrawals." }
        period_start: { type: string, nullable: true }
        period_end: { type: string, nullable: true }
        cagr: { type: number, nullable: true, description: "Compound annual growth rate (%) over the window." }
        volatility: { type: number, nullable: true, description: "Return volatility over the window." }
        sharpe: { type: number, nullable: true, description: "Sharpe ratio over the window." }
        sortino: { type: number, nullable: true, description: "Sortino ratio over the window." }
    RatePoint:
      type: object
      properties:
        timestamp: { type: string }
        apr: { type: number, nullable: true }
        apy: { type: number, nullable: true }
        apr_30d_ma: { type: number, nullable: true }
        apy_30d_ma: { type: number, nullable: true }
    TBillPoint:
      type: object
      properties:
        timestamp: { type: string }
        rate: { type: number, nullable: true }
    Holder:
      type: object
      properties:
        rank: { type: integer, description: "1-based position by balance, descending." }
        address: { type: string }
        balance: { type: number, description: "Raw share-token units; loses precision above 2^53." }
        share_pct: { type: number, nullable: true, description: "This address's share of all positive balances, %. null when the total is not known." }
        cumulative_pct: { type: number, nullable: true, description: "Share of this address and every address ranked above it, %." }
    HolderListMeta:
      type: object
      properties:
        strategy_id: { type: integer }
        holders: { type: integer, nullable: true, description: "Holder count; null when the vault has never been scanned with the store." }
        total_balance: { type: number, nullable: true }
        total_known: { type: boolean }
        page:
          type: object
          properties:
            limit: { type: integer }
            offset: { type: integer }
            total_rows: { type: integer, nullable: true }
        scan:
          type: object
          nullable: true
          description: The scan checkpoint the list is consistent with; null = not scanned yet.
          properties:
            status: { type: string }
            last_committed_block: { type: integer }
            full_scan_at: { type: string, nullable: true }
            updated_at: { type: string }
    Concentration:
      type: object
      description: Holder concentration of the vault's share-token map, computed by the scanner at the block the scan completed. Shares are fractions in [0, 1].
      properties:
        block_number: { type: integer }
        computed_at: { type: string }
        holders: { type: integer, description: "Addresses with a positive balance." }
        total_balance: { type: number, description: "Sum of positive balances, raw share-token units (loses precision above 2^53)." }
        decimals: { type: integer, nullable: true, description: "Share-token decimals; null for Uniswap liquidity or when unreadable." }
        top1_share: { type: number, description: "Share held by the largest address." }
        top5_share: { type: number }
        top10_share: { type: number }
        hhi: { type: number, description: "Herfindahl-Hirschman index, sum of squared shares; 1 = a single holder." }
        gini: { type: number, description: "Gini coefficient of positive balances; 0 = all holders equal." }
        holders_gt_1pct: { type: integer, description: "Addresses holding more than 1% of the total." }
        deposit_events: { type: integer, description: "Deposit events over the indexed life — the same figure as Vault.deposit_count." }
        active_addresses_30d: { type: integer, nullable: true, description: "Distinct depositors in the trailing 30 days — the same figure as Vault.active_addresses_30d." }
        window_start_block: { type: integer, nullable: true, description: "First block of that 30-day window." }
    VaultRisk:
      type: object
      properties:
        strategy_id: { type: integer }
        assessed: { type: boolean, description: "false = no published assessment; the other fields are then null / empty." }
        band: { type: string, nullable: true, description: "A (safest) .. F." }
        score: { type: number, nullable: true, description: "0-100, higher = safer." }
        date: { type: string, nullable: true, description: "Date of the latest published row." }
        methodology_version: { type: string, nullable: true }
        bands:
          type: array
          description: Band thresholds under the current methodology, e.g. [["A",85],["B",70],...].
          items: { type: array, items: {}, minItems: 2, maxItems: 2 }
        floors:
          type: array
          description: Binding hard floors on the latest row — the standing warning strip.
          items:
            type: object
            properties:
              key: { type: string }
              label: { type: string }
        range: { type: string }
        from: { type: string, nullable: true }
        to: { type: string, nullable: true }
        history:
          type: array
          items:
            type: object
            properties:
              date: { type: string }
              band: { type: string, nullable: true }
              score: { type: number, nullable: true }
              methodology_version: { type: string }
              event_flags: { type: array, items: { type: string } }
        events:
          type: array
          items:
            type: object
            properties:
              date: { type: string }
              event_type: { type: string, enum: [methodology_update, overlay, hard_floor, band_transition, contagion_flag] }
              title: { type: string }
              is_global: { type: boolean }
        full:
          nullable: true
          x-plan: custom
          allOf: [{ $ref: "#/components/schemas/RiskFull" }]
          description: CUSTOM. The decomposition; null below Custom (with plan_required).
        plan_required: { $ref: "#/components/schemas/PlanRequired" }
    RiskFull:
      type: object
      description: The full decomposition behind the score, rebuilt from the published vector.
      properties:
        methodology_version: { type: string }
        raw_band: { type: string, nullable: true, description: "Band the score sits in before dwell; may differ from the published band." }
        blend:
          type: object
          properties:
            rosette_weight: { type: number }
            anchor_weight: { type: number }
        rosette:
          type: object
          properties:
            score: { type: number, nullable: true }
            dimensions:
              type: array
              items:
                type: object
                properties:
                  key: { type: string }
                  label: { type: string }
                  weight: { type: number }
                  score: { type: number }
                  points: { type: number }
                  max: { type: number }
                  subscores:
                    type: array
                    items:
                      type: object
                      properties:
                        key: { type: string }
                        label: { type: string }
                        position: { type: string, nullable: true }
                        points: { type: number, nullable: true }
                        max: { type: number, nullable: true }
                        applicable: { type: boolean }
                        precedent: { type: string, nullable: true }
        anchor:
          type: object
          properties:
            score: { type: number, nullable: true }
            points: { type: number, nullable: true }
            max: { type: number, nullable: true }
            inputs:
              type: array
              items:
                type: object
                properties:
                  key: { type: string }
                  label: { type: string }
                  value: { type: number, nullable: true }
                  unit: { type: string, nullable: true }
                  band: { type: string }
                  points: { type: number }
                  max: { type: number }
        dependency_cap:
          type: object
          properties:
            cap: { type: number, nullable: true }
            binding: { type: boolean }
            critical_dep_min: { type: number, nullable: true }
            slack: { type: number, nullable: true }
            critical_edges: { type: array, items: { type: string } }
            edges:
              type: array
              items:
                type: object
                properties:
                  type: { type: string }
                  node_id: { type: string }
                  label: { type: string, nullable: true }
                  node_type: { type: string, nullable: true }
                  critical: { type: boolean }
                  rated: { type: boolean }
                  rating: { type: number, nullable: true }
                  unrated_reason: { type: string, nullable: true }
        overlays:
          type: object
          properties:
            clamp: { type: number, nullable: true }
            net: { type: number }
            applied:
              type: array
              items:
                type: object
                properties:
                  key: { type: string }
                  delta: { type: number }
                  reason: { type: string, nullable: true }
                  expires: { type: string, nullable: true }
        floors:
          type: array
          items:
            type: object
            properties:
              key: { type: string }
              label: { type: string }
              cap: { type: number, nullable: true }
              applied_cap: { type: number, nullable: true }
              binding: { type: boolean }
        computation:
          type: object
          properties:
            s_raw: { type: number, nullable: true }
            overlays_net: { type: number, nullable: true }
            cap_applied: { type: number, nullable: true }
            final: { type: number, nullable: true }
        history:
          type: array
          description: Per-day private series over the same window as the Basic history.
          items:
            type: object
            properties:
              date: { type: string }
              anchor_score: { type: number, nullable: true }
              rosette_score: { type: number, nullable: true }
              dependency_cap: { type: number, nullable: true }
    Hack:
      type: object
      description: A single hack / loss event.
      properties:
        id: { type: integer }
        date: { type: string, description: "ISO 8601 timestamp of the event." }
        name: { type: string, description: "Protocol (DeFi) or institution (TradFi) that suffered the loss." }
        amount_hacked: { type: number, description: "Amount lost, in USD." }
        category: { type: string, enum: [defi, tradfi] }
        type:
          type: string
          enum: [DeFi, Dexes, Bridges]
          description: >
            What kind of protocol was hit: DeFi (lending, yield, staking,
            stablecoins), Dexes, or Bridges. Present on DeFi events only —
            TradFi events are bank failures, which this taxonomy does not
            describe, so they omit the field.
security:
  - bearerAuth: []
