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_….
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
nulland is named in a top-levelplan_requiredobject 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" }.
| Parameter | Type | Description |
|---|---|---|
| Vault.holders | Free | Holder count. |
| GET /vaults/:id/holders | Pro | Holder list + concentration. 403 plan_required on Free. |
| Vault.deposit_count / active_addresses_30d / tvl_concentration_top10pct | Pro | Holder analytics. null + plan_required on Free. |
| GET /vaults/:id/risk — band, score, history, floors | Free | Basic risk, every plan. |
| GET /vaults/:id/risk — full | Custom | Rosette, anchor, dependency cap, decomposition. null + plan_required below Custom. |
Endpoints
/api/v1/auth/tokenNo authExchange an API key for a short-lived bearer token.
Request body (JSON)
| Parameter | Type | Description |
|---|---|---|
| apiKey | string | Your secret API key. |
Response
{ "token": "<JWT>", "expiresIn": 3600, "plan": "free" }/api/v1/vaultsBearer tokenList vaults with optional filters and pagination.
Query parameters (all optional)
| Parameter | Type | Description |
|---|---|---|
| protocol_name | string | Filter by protocol (e.g. Aave, Morpho, Euler, Uniswap). |
| chain_id | number | Filter by chain id (e.g. 1, 8453, 42161). |
| strategy_id | number | Filter by a strategy (compat) id. |
| tvl_filter | string | TVL band, e.g. gte_1m, gte_5m, non_na, na. |
| apr_filter | string | APR band, e.g. gt_5, gt_10, lte_10, non_na, na. |
| age_filter | string | Pool age, e.g. new, 3mo, 6mo, 12mo, non_na, na. |
| asset_class | string | Denomination class: stable, mixed, non-stable. Comma-separate for several. |
| search | string | Case-insensitive substring match on the vault name, e.g. USDC, WETH, Gauntlet. |
| risk_band | string | Keep only these published bands, e.g. A,B. Unrated vaults never match. |
| min_risk_score | number | Keep vaults whose published risk_score is at least this (0–100). Unrated vaults never match. |
| sort | string | id_asc (default), apr_desc, tvl_desc, risk_score_desc, risk_adjusted_apr_desc. Nulls sort last. Unknown values return 400 invalid_sort. |
| limit | number | Page size. Default 100, max 1000. |
| offset | number | Rows 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
}/api/v1/vaults/:idBearer tokenFetch a single vault by its pool id. Returns 404 if not found.
Response
{ "data": { /* Vault */ }, "plan_required": { /* Free plan only, see Plans */ } }/api/v1/vaults/:id/historyBearer tokenDaily time-series for a vault. Here :id is the strategy id (strategy_id on the Vault object).
Query parameters
| Parameter | Type | Description |
|---|---|---|
| range | string | Window: 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.
/api/v1/vaults/:id/statsBearer tokenAggregated stats over fixed windows for a vault (:id = strategy id). Omit period to get all windows.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| period | string | weekly, 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.
/api/v1/vaults/:id/holdersBearer tokenPro 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
| Parameter | Type | Description |
|---|---|---|
| limit | number | Page size. Default 100, max 500. |
| offset | number | Rows 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.
/api/v1/vaults/:id/riskBearer tokenpigi'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
| Parameter | Type | Description |
|---|---|---|
| range | string | History 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.
/api/v1/ratesBearer tokenDaily 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 } ]
}/api/v1/hacksBearer tokenHack 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)
| Parameter | Type | Description |
|---|---|---|
| category | string | defi, tradfi, or all. Default all. |
| type | string | DeFi, Dexes, or Bridges (case-insensitive). DeFi events only — TradFi rows carry no type, so this excludes them. |
| from | string | Only events on/after this date — YYYY-MM-DD or ISO 8601. |
| to | string | Only events on/before this date (inclusive of the whole day). |
| min_amount | number | Only events at or above this USD amount. |
| sort | string | date_asc (default), date_desc, amount_desc, or amount_asc. |
| limit | number | Page size. Default 100, max 1000. |
| offset | number | Rows 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
/api/v1/usageBearer tokenYour 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.
/api/v1No authDiscovery index — lists the available endpoints and the docs URL.
The Vault object
Returned by /vaults and /vaults/:id.
| Parameter | Type | Description |
|---|---|---|
| id | number | Pool id (use for /vaults/:id). |
| strategy_id | number | Strategy id (use for /history and /stats). |
| protocol_name | string | Protocol, e.g. Aave, Morpho, Euler. |
| chain_id | number | Chain id. |
| pool_name | string | The catalogue / on-chain vault name. Not unique — e.g. "EVK Vault eUSDC-2" exists on five chains. |
| display_name | string | null | pool_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_address | string | On-chain pool/vault address. |
| asset_address | string | null | Underlying asset address. |
| asset_class | string | null | Denomination class of the vault's assets: stable, mixed, or non-stable. |
| type | string | Pool-type category (Lending, AMM, …). |
| tvl_30d_ma | number | 30-day moving-average TVL (USD). |
| apr_30d_ma | number | 30-day moving-average APR (%). |
| holders | number | null | Holder count (every plan). |
| pool_creation_date | string | null | ISO date the pool was created. |
| updated_at | string | null | Block-time of the newest data point. |
| tvl_flow_1d | number | null | 1-day TVL flow. |
| tvl_flow_7d | number | null | 7-day TVL flow. |
| apr_trend_1d | number | null | 1-day APR trend. |
| risk_band | string | null | Published pigi risk band: A (safest) … F. null = not assessed yet. |
| risk_score | number | null | Published overall risk score, 0–100 (higher = safer). null = not assessed yet. |
| risk_adjusted_apr | number | null | apr_30d_ma minus the risk penalty derived from the published score (the same adjustment as ra_apr in /history). null when unrated or without APR. |
| active_addresses_30d | number | null | Pro. 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_count | number | null | Pro. 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_top10pct | number | null | Pro. % 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.
| Parameter | Type | Description |
|---|---|---|
| 400 | missing_api_key / invalid_id | Malformed request. |
| 401 | invalid_api_key / invalid_token | Bad key on exchange, or missing/expired token. |
| 403 | client_suspended | Your API client is suspended. |
| 403 | plan_required | The endpoint needs a higher plan — `required` names it (see Plans). |
| 404 | not_found | No vault with that id. |
| 405 | method_not_allowed | Wrong HTTP method for the route. |
| 429 | rate_limit_exceeded | Monthly request limit reached — see RateLimit-* / Retry-After; resets at the start of next month (UTC). |
| 500 | internal_error | Unexpected server error. |
| 503 | risk_unavailable | /vaults/:id/risk only: the risk database could not be reached. Retry. |