API Documentation

    The pigi.finance Data API (v1) gives programmatic access to the same DeFi vault data that powers this site — current metrics, daily history, aggregated stats, benchmark rates, and hack/loss history. All responses are JSON and CORS is open, so you can call it from a server or a browser.

    Need a key? Request access on the API page. You'll receive a key like pigi_….

    Base URL
    https://pigi.finance/api/v1

    Machine-readable spec: OpenAPI 3 (openapi.yaml) — import it into Postman, Swagger UI, or your codegen of choice.

    Authentication

    Auth is a two-step API key → token exchange. Your long-lived API key is secret; you trade it for a short-lived JWT (≈1 hour) and send that as a Bearer token on every request. When the token expires, exchange the key again.

    # 1. Exchange your API key for a short-lived token
    curl -X POST https://pigi.finance/api/v1/auth/token \
      -H "Content-Type: application/json" \
      -d '{"apiKey":"pigi_live_xxxxxxxxxxxxxxxx"}'
    
    # → { "token": "<JWT>", "expiresIn": 3600, "plan": "free" }
    
    # 2. Call any data endpoint with the token
    curl "https://pigi.finance/api/v1/vaults?limit=5" \
      -H "Authorization: Bearer <JWT>"

    Keep your API key server-side. The short-lived token is safe to use from the browser. A token from this API can't be used on any other pigi endpoint and vice-versa.

    Plans

    Three plans — Free, Pro, Custom — see the comparison table. Every plan gets every rate, TVL and window figure, the holder count, and Basic risk (band, overall score and their history — what the vault page shows to anyone). Pro adds the holder list with its concentration figures, and the holder-analytics fields on the Vault object. Custom adds the full risk decomposition. Your plan is echoed by /auth/token and /usage.

    Gating is explicit, so "not on your plan" is never mistaken for "no data":

    • A gated field comes back null and is named in a top-level plan_required object on the response, e.g. "plan_required": { "deposit_count": "pro" }. A field that is null for another reason (not indexed yet, vault not assessed) carries no marker.
    • A gated endpoint answers 403 { "error": "plan_required", "required": "pro" }.
    ParameterTypeDescription
    Vault.holdersFreeHolder count.
    GET /vaults/:id/holdersProHolder list + concentration. 403 plan_required on Free.
    Vault.deposit_count / active_addresses_30d / tvl_concentration_top10pctProHolder analytics. null + plan_required on Free.
    GET /vaults/:id/risk — band, score, history, floorsFreeBasic risk, every plan.
    GET /vaults/:id/risk — fullCustomRosette, anchor, dependency cap, decomposition. null + plan_required below Custom.

    Endpoints

    POST/api/v1/auth/tokenNo auth

    Exchange an API key for a short-lived bearer token.

    Request body (JSON)

    ParameterTypeDescription
    apiKeystringYour secret API key.

    Response

    { "token": "<JWT>", "expiresIn": 3600, "plan": "free" }
    GET/api/v1/vaultsBearer token

    List vaults with optional filters and pagination.

    Query parameters (all optional)

    ParameterTypeDescription
    protocol_namestringFilter by protocol (e.g. Aave, Morpho, Euler, Uniswap).
    chain_idnumberFilter by chain id (e.g. 1, 8453, 42161).
    strategy_idnumberFilter by a strategy (compat) id.
    tvl_filterstringTVL band, e.g. gte_1m, gte_5m, non_na, na.
    apr_filterstringAPR band, e.g. gt_5, gt_10, lte_10, non_na, na.
    age_filterstringPool age, e.g. new, 3mo, 6mo, 12mo, non_na, na.
    limitnumberPage size. Default 100, max 1000.
    offsetnumberRows to skip. Default 0.

    Response

    {
      "data": [ { /* Vault */ } ],
      "pagination": { "total": 1342, "limit": 100, "offset": 0, "hasMore": true },
      "availableFilters": { "protocol_names": [...], "chain_ids": {...}, "all_chain_ids": [...] },
      "plan_required": { "deposit_count": "pro", "active_addresses_30d": "pro",
                         "tvl_concentration_top10pct": "pro" }   // Free plan only
    }
    GET/api/v1/vaults/:idBearer token

    Fetch a single vault by its pool id. Returns 404 if not found.

    Response

    { "data": { /* Vault */ }, "plan_required": { /* Free plan only, see Plans */ } }
    GET/api/v1/vaults/:id/historyBearer token

    Daily time-series for a vault. Here :id is the strategy id (strategy_id on the Vault object).

    Query parameters

    ParameterTypeDescription
    rangestringWindow: 7D, 30D, 90D, or 180D. Default 7D.

    Response

    {
      "data": [
        { "timestamp": "2026-06-23T00:00:00Z", "tvl": 12500000, "apr": 6.21,
          "apy": 6.40, "ra_apr": 5.11, "tvl_30d_ma": 12100000, "apr_30d_ma": 6.05,
          "apy_30d_ma": 6.18 }
      ],
      "lastTvl": 12500000, "lastApr": 6.21, "lastApy": 6.40, "lastRaApr": 5.11,
      "lastTvl30dMa": 12100000, "lastApr30dMa": 6.05, "lastApy30dMa": 6.18, "lastRaApr30dMa": 4.95
    }

    ra_apr is risk-adjusted APR: APR minus a penalty derived from the vault's published overall risk score (0–100, higher = safer); it is null when the vault has no published assessment.

    The *_30d_ma fields are simple arithmetic means of the last 30 daily values. For APR that equals the 30-day window rate (APR is linear). For APY it does not: apy_30d_ma is the average of 30 annualized daily rates, not the return you would get by compounding those 30 days together. The two coincide on stable vaults and diverge on volatile ones — averaging convex daily APYs overstates the realized figure, by more the wider the day-to-day swings. For the return a holder actually experienced, use /stats: its window apy and the NAV-based cagr are point-to-point. The same definition applies to the base-rate series below.

    GET/api/v1/vaults/:id/statsBearer token

    Aggregated stats over fixed windows for a vault (:id = strategy id). Omit period to get all windows.

    Query parameters

    ParameterTypeDescription
    periodstringweekly, monthly, quarterly, yearly, or lifetime (optional).

    Response

    {
      "weekly":  { "tvl_low": 11.8e6, "tvl_high": 12.6e6, "apr": 6.1, "apy": 6.3,
                   "inflows": 2.1e5, "period_start": "...", "period_end": "...",
                   "cagr": 6.2, "volatility": 1.4, "sharpe": 1.8, "sortino": 2.3,
                   "nav_period_start": null, "nav_period_end": null },
      "monthly": { ... }, "quarterly": { ... }, "yearly": { ... },
      "lifetime": { ... }
    }

    inflows is the net TVL change over the window (last day − first day): positive = net inflow, negative = net outflow. It is TVL-based, so it also reflects yield earned over the window, not just deposits/withdrawals. Each window also includes risk/return metrics — cagr, volatility, sharpe, and sortino — computed from the vault's share-price (NAV) series; they are null for vaults without one (e.g. Uniswap pools) — plus a lifetime window — which covers every day pigi has indexed the vault, so its period_start is our first indexed day rather than the vault's launch. On lifetime, read those four against nav_period_start / nav_period_end rather than the period: apr and apy cover every day with a quoted rate, while the share-price series is often shorter.

    GET/api/v1/vaults/:id/holdersBearer token

    Pro plan. The vault's holder list — :id = strategy id — ranked by share-token balance, with each address's share of the total and the cumulative share down the ranking. A Free client gets 403 { "error": "plan_required", "required": "pro" }. (Vault.holders gives the holder count on every plan.)

    Query parameters

    ParameterTypeDescription
    limitnumberPage size. Default 100, max 500.
    offsetnumberRows to skip. Default 0.

    Response

    {
      "data": [
        { "rank": 1, "address": "0x1a2b…c3d4", "balance": 1.2345e+22,
          "share_pct": 31.2, "cumulative_pct": 31.2 }
      ],
      "meta": {
        "strategy_id": 4321, "holders": 812, "total_balance": 3.9e+22, "total_known": true,
        "page": { "limit": 100, "offset": 0, "total_rows": 812 },
        "scan": { "status": "completed", "last_committed_block": 23456789,
                  "full_scan_at": "2026-09-01T02:10:00Z", "updated_at": "2026-09-06T02:12:41Z" },
        "concentration": { "block_number": 23456789, "holders": 812, "total_balance": 3.9e+22,
                           "decimals": 18, "top1_share": 0.312, "top5_share": 0.654,
                           "top10_share": 0.781, "hhi": 0.1421, "gini": 0.83, "holders_gt_1pct": 14,
                           "deposit_events": 2310, "active_addresses_30d": 57, "window_start_block": 23240789 }
      }
    }

    The list is replayed from on-chain transfers by pigi's holders scan. A vault that has not been scanned with per-address storage yet 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, not the raw figure. Above 20,000 holders the total is not walked and share_pct is null with total_known: false. meta.concentration is the map's concentration at the scan's block — top-1 / top-5 / top-10 share (fractions of all positive balances), HHI (sum of squared shares, 1 = a single holder), Gini (0 = all holders equal) and the count of addresses above 1% — or null until the vault's next scan writes it.

    GET/api/v1/vaults/:id/riskBearer token

    pigi's published risk assessment for a vault — :id = strategy id. Every plan gets the Basic block: the band and overall score (0–100, higher = safer), their date-keyed history, the band thresholds under the current methodology, the 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, whether it binds), 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 with "plan_required": { "full": "custom" }.

    Query parameters

    ParameterTypeDescription
    rangestringHistory window: 7D, 30D, 90D, 180D, 365D or ALL. Default 90D.

    Response

    {
      "strategy_id": 25, "assessed": true,
      "band": "B", "score": 74.2, "date": "2026-09-06", "methodology_version": "1.1.0",
      "bands": [["A", 85], ["B", 70], ["C", 55], ["D", 40], ["F", 0]],
      "floors": [],
      "range": "90D", "from": "2026-06-09", "to": "2026-09-06",
      "history": [ { "date": "2026-09-06", "band": "B", "score": 74.2,
                     "methodology_version": "1.1.0", "event_flags": [] } ],
      "events":  [ { "date": "2026-08-14", "event_type": "hard_floor",
                     "title": "Redemptions paused", "is_global": false } ],
      "full": null,                            // Custom: the decomposition (see below)
      "plan_required": { "full": "custom" }    // absent on Custom
    }
    
    // full (Custom plan):
    { "methodology_version": "1.1.0", "raw_band": "B",
      "blend": { "rosette_weight": 0.6, "anchor_weight": 0.4 },
      "rosette": { "score": 68, "dimensions": [ { "key": "contract", "label": "Contract & platform security",
                   "weight": 0.22, "score": 72, "points": 18, "max": 25,
                   "subscores": [ { "key": "audit_coverage", "label": "Audit coverage & recency",
                                    "position": "audited, <12mo", "points": 7, "max": 9,
                                    "applicable": true, "precedent": "…" } ] } ] },
      "anchor": { "score": 77, "points": 77, "max": 100,
                  "inputs": [ { "key": "tvl", "label": "TVL", "value": 306060998, "unit": "usd",
                                "band": "$100–500M", "points": 20, "max": 25 } ] },
      "dependency_cap": { "cap": 82, "binding": false, "critical_dep_min": 72, "slack": 10,
                          "critical_edges": ["DEPOSITS_INTO", "PRICED_BY"],
                          "edges": [ { "type": "PRICED_BY", "node_id": "chainlink-usdc-usd",
                                       "label": "Chainlink USDC/USD", "node_type": "oracle",
                                       "critical": true, "rated": true, "rating": 72,
                                       "unrated_reason": null } ] },
      "overlays": { "clamp": 8, "net": 0, "applied": [] },
      "floors": [],
      "computation": { "s_raw": 71.6, "overlays_net": 0, "cap_applied": null, "final": 74.2 },
      "history": [ { "date": "2026-09-06", "anchor_score": 77, "rosette_score": 68, "dependency_cap": 82 } ] }

    assessed: false (with null / empty fields) means the vault has no published assessment — most vaults. 503 risk_unavailable means the risk database could not be reached; retry.

    GET/api/v1/ratesBearer token

    Daily history of the DeFi Base Rate — the mean yield across the tracked stablecoin (stable) and ETH (eth) vault sets — plus the 3-month U.S. T-Bill risk-free rate (tbills). No parameters; each series is sorted oldest to newest.

    Response

    {
      "stable": [ { "timestamp": "2026-06-23T00:00:00Z", "apr": 5.4, "apy": 5.55,
                    "apr_30d_ma": 5.2, "apy_30d_ma": 5.34 } ],
      "eth":    [ { "timestamp": "2026-06-23T00:00:00Z", "apr": 3.1, "apy": 3.15,
                    "apr_30d_ma": 3.0, "apy_30d_ma": 3.05 } ],
      "tbills": [ { "timestamp": "2026-06-23T00:00:00Z", "rate": 3.79 } ]
    }
    GET/api/v1/hacksBearer token

    Hack and loss events — every tracked DeFi exploit plus a reference set of major TradFi banking losses, the dataset behind our DeFi vs TradFi losses analysis. Each event carries its date, the protocol or institution, the amount lost in USD, and — for DeFi — what kind of protocol was hit.

    Query parameters (all optional)

    ParameterTypeDescription
    categorystringdefi, tradfi, or all. Default all.
    typestringDeFi, Dexes, or Bridges (case-insensitive). DeFi events only — TradFi rows carry no type, so this excludes them.
    fromstringOnly events on/after this date — YYYY-MM-DD or ISO 8601.
    tostringOnly events on/before this date (inclusive of the whole day).
    min_amountnumberOnly events at or above this USD amount.
    sortstringdate_asc (default), date_desc, amount_desc, or amount_asc.
    limitnumberPage size. Default 100, max 1000.
    offsetnumberRows to skip. Default 0.

    Response

    {
      "data": [
        { "id": 1937, "date": "2022-03-28T23:00:00+00:00", "name": "Ronin",
          "amount_hacked": 624000000, "category": "defi", "type": "Bridges" }
      ],
      "summary": { "count": 285, "total_amount_hacked": 18824475500,
                   "first_date": "2020-09-28T23:00:00+00:00",
                   "last_date": "2026-07-10T00:00:00+00:00" },
      "pagination": { "total": 285, "limit": 100, "offset": 0, "hasMore": true }
    }

    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.

    type is present on DeFi events only, and is one of DeFi (lending, yield, staking, stablecoins), Dexes, or Bridges. TradFi events are bank failures, which the taxonomy does not describe, so they omit the field.

    Example — DeFi losses in 2025, biggest first

    curl -H "Authorization: Bearer $TOKEN" \
      "https://pigi.finance/api/v1/hacks?category=defi&from=2025-01-01&to=2025-12-31&sort=amount_desc"

    Example — total ever lost to bridge exploits

    curl -H "Authorization: Bearer $TOKEN" \
      "https://pigi.finance/api/v1/hacks?type=bridges&limit=1"   # read summary.total_amount_hacked
    GET/api/v1/usageBearer token

    Your request count for the current calendar month (UTC) and your plan limit.

    Response

    {
      "clientId": 1, "plan": "free",
      "period": { "start": "2026-06-01T00:00:00Z", "end": "2026-07-01T00:00:00Z" },
      "used": 1423, "limit": 100000, "remaining": 98577
    }

    limit and remaining are null when your plan has no cap.

    GET/api/v1No auth

    Discovery index — lists the available endpoints and the docs URL.

    The Vault object

    Returned by /vaults and /vaults/:id.

    ParameterTypeDescription
    idnumberPool id (use for /vaults/:id).
    strategy_idnumberStrategy id (use for /history and /stats).
    protocol_namestringProtocol, e.g. Aave, Morpho, Euler.
    chain_idnumberChain id.
    pool_namestringThe catalogue / on-chain vault name. Not unique — e.g. "EVK Vault eUSDC-2" exists on five chains.
    display_namestring | nullpool_name plus the minimum qualifier (chain · symbol · framework) that makes it unique. Prefer this for display; null for a vault added since the last nightly.
    pool_addressstringOn-chain pool/vault address.
    asset_addressstring | nullUnderlying asset address.
    typestringPool-type category (Lending, AMM, …).
    tvl_30d_manumber30-day moving-average TVL (USD).
    apr_30d_manumber30-day moving-average APR (%).
    holdersnumber | nullHolder count (every plan).
    pool_creation_datestring | nullISO date the pool was created.
    updated_atstring | nullBlock-time of the newest data point.
    tvl_flow_1dnumber | null1-day TVL flow.
    tvl_flow_7dnumber | null7-day TVL flow.
    apr_trend_1dnumber | null1-day APR trend.
    risk_bandstring | nullPublished pigi risk band: A (safest) … F. null = not assessed yet.
    risk_scorenumber | nullPublished overall risk score, 0–100 (higher = safer). null = not assessed yet.
    active_addresses_30dnumber | nullPro. Distinct depositors in the trailing 30 days ending at the last scan: owners on ERC-4626 Deposit logs; for Uniswap V3 the current owners of positions minted in the window. null + plan_required on Free; null without a marker until the vault has been scanned (Aave reserves are not scanned).
    deposit_countnumber | nullPro. Deposit events over the vault's indexed life: ERC-4626 Deposit logs (Morpho, Euler, Yearn), pool Mint logs for Uniswap V3; withdrawals not counted. null + plan_required on Free; null without a marker until the vault has been scanned (Aave reserves are not scanned).
    tvl_concentration_top10pctnumber | nullPro. % of TVL held by the top 10% of holders. null + plan_required on Free; null without a marker while holder analytics are still being indexed.

    Errors & conventions

    Errors are JSON: { "error": "<code>" }. Successful data responses are cached for 5 minutes (Cache-Control: public, max-age=300); auth and usage responses are never cached. Every authenticated request counts toward your monthly usage. Each response carries RateLimit-Limit/RateLimit-Remaining/RateLimit-Reset; once you reach your plan's monthly limit, requests return 429 (with Retry-After) until it resets at the start of the next UTC month.

    ParameterTypeDescription
    400missing_api_key / invalid_idMalformed request.
    401invalid_api_key / invalid_tokenBad key on exchange, or missing/expired token.
    403client_suspendedYour API client is suspended.
    403plan_requiredThe endpoint needs a higher plan — `required` names it (see Plans).
    404not_foundNo vault with that id.
    405method_not_allowedWrong HTTP method for the route.
    429rate_limit_exceededMonthly request limit reached — see RateLimit-* / Retry-After; resets at the start of next month (UTC).
    500internal_errorUnexpected server error.
    503risk_unavailable/vaults/:id/risk only: the risk database could not be reached. Retry.