> For the complete documentation index, see [llms.txt](https://docs.ipor.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipor.io/build-on-fusion/developer-guide/api.md).

# API

## IPOR Static API — Developer Guide

Public, read-only JSON API serving IPOR Fusion vault data, including the historical time series behind the charts on [app.ipor.io](https://app.ipor.io).

**Base URL:** `https://api.ipor.io`

No API key. No authentication. No rate limit. No query parameters — every input is encoded in the URL path.

> **Important:** the long-range history buckets (`1y`, `all`) are **intentionally stale** by days to weeks. A request for `-all` alone returns a series whose most recent point may be around three weeks old, with no error to indicate this. See Period buckets and freshness. This behaviour is by design and will not change; clients are expected to merge buckets.

***

### Quick start

Chart a vault's APY over the last 30 days:

```bash
# Note the lowercase address. Anything else 404s.
curl --compressed \
  'https://api.ipor.io/v2/fusion/vault-history/1/0x43ee0243ea8cf02f7087d8b16c8d2007cc9c7ca2-30d' \
  | jq '.history[-1] | {blockTimestamp, apy, tvl}'
```

```json
{
  "blockTimestamp": "2026-07-08T13:14:47Z",
  "apy": "3.2864762350246543",
  "tvl": "490462.27822644341523"
}
```

Note that the most recent point is already approximately 21 hours old. Obtaining a series that extends to the present requires additionally fetching `-7d` and merging the two. The reference client performs this automatically.

Discover vault addresses first:

```bash
curl --compressed 'https://api.ipor.io/dapp/plasma-vaults-list' \
  | jq '.plasmaVaults[] | {chainId, address, name, apr}'
```

***

### Conventions

**Addresses must be lowercase.** The API is static JSON objects on S3 behind CloudFront, so paths are object keys and are case-sensitive. A checksummed (mixed-case) address returns `404`.

```
200  /v2/fusion/vault-history/1/0x43ee0243ea8cf02f7087d8b16c8d2007cc9c7ca2-7d
404  /v2/fusion/vault-history/1/0x43Ee0243eA8CF02f7087d8B16C8D2007cC9c7cA2-7d
```

This requires care, because **`/dapp/plasma-vaults-list` returns most of its addresses checksummed** (mixed-case). An address taken from the vault list and used unmodified in a history path will return 404. Apply `.toLowerCase()` before constructing any path.

**Numbers are decimal strings, not JSON numbers.** `"tvl": "490462.27822644341523"`. The precision exceeds that of an IEEE-754 double. Parse them with a decimal library (`decimal.js`, `big.js`, `bignumber.js`). Passing them through `Number()` will discard low-order digits. Token amounts (`totalAssets`, `balance`) are raw base units and must be scaled by the asset's `assetDecimals`.

**Timestamps are ISO-8601 UTC**, e.g. `"2026-07-09T09:14:47Z"`.

**Every history point carries a `blockNumber`**, which is the correct key for joining, deduplicating, and ordering series. Prefer it over the timestamp.

**Errors are XML, not JSON.** A miss — unknown vault, unsupported period, or a non-lowercase address — returns the raw S3 error document with `content-type: application/xml`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message>...</Error>
```

A client that calls `.json()` on any non-2xx response will throw a parse error rather than surface a clean 404. Branch on `response.status` before parsing.

**Responses are gzipped** (`content-encoding: gzip`). Send `Accept-Encoding: gzip` (`curl --compressed`); browsers and `fetch` do this automatically. The uncompressed payloads are much larger — see Payload sizes.

**CORS is open** (`access-control-allow-origin: *`, `GET` only). The header is returned when an `Origin` request header is present, so browser calls work directly with no proxy.

***

### Period buckets and freshness

Both history endpoints take a period suffix on the last path segment:

| Period | Coverage            |
| ------ | ------------------- |
| `7d`   | Trailing 7 days     |
| `30d`  | Trailing 30 days    |
| `1y`   | Trailing 365 days   |
| `all`  | Full vault lifetime |

Points are spaced roughly **hourly**.

#### Why the data is spliced this way

The API is not a database query layer. Every response is a **pre-rendered file written to S3** and served through a CDN. This is a deliberate design decision made for security and performance: there is no query surface to exploit or overload, and reads remain fast and inexpensive at any traffic level.

The trade-off is that a file's contents are only as current as its last write, and rewriting a multi-megabyte object is costly. Consequently, **the older a file's data, the less frequently it is regenerated**. The `7d` file is small and is rewritten frequently. The `all` file is large and consists mostly of immutable history, so it is rewritten rarely.

This is the reason the most recent point in a bucket lags real time by an amount that grows with the size of the bucket. Measured against vault `0x43ee0243ea8cf02f7087d8b16c8d2007cc9c7ca2` on Ethereum at `2026-07-09T10:00Z`:

| Period | Points | Newest point        | Trails real time by |
| ------ | ------ | ------------------- | ------------------- |
| `7d`   | 168    | `2026-07-09T09:14Z` | **\~45 minutes**    |
| `30d`  | 720    | `2026-07-08T13:14Z` | **\~21 hours**      |
| `1y`   | 8,714  | `2026-07-06T13:14Z` | **\~2.9 days**      |
| `all`  | 14,726 | `2026-06-17T13:14Z` | **\~22 days**       |

The exact lag varies by vault and by regeneration cycle. Do not depend on the specific values above. Depend instead on the ordering, which is invariant: `7d` is always the most current bucket and `all` is always the least current.

**This is intended behaviour and will not change.** Rewriting the multi-megabyte `all` object every hour would provide no benefit, because its stale tail can be reconstructed inexpensively from the smaller, more current buckets. Clients are expected to perform this reconstruction.

#### The merge rule

To render a period accurately, fetch the requested bucket **plus every more current bucket that covers its stale tail**, then merge the point sets:

| Requested | Fetch              |
| --------- | ------------------ |
| `7d`      | `7d`               |
| `30d`     | `30d`, `7d`        |
| `1y`      | `1y`, `30d`, `7d`  |
| `all`     | `all`, `30d`, `7d` |

Merge by `blockNumber`, sort ascending, and on a duplicate `blockNumber` retain the point from the more current bucket. The buckets overlap, so duplicates are expected and must be collapsed. Otherwise any aggregate computed over the series (averages, integrals, point counts) will double-count the overlap region.

Applying this, all four periods become current to within \~1 hour:

```
  7d  pts=   168  first=2026-07-02T11:14:47Z  last=2026-07-09T10:14:47Z  trails=0.2h
 30d  pts=   741  first=2026-06-08T14:14:47Z  last=2026-07-09T10:14:47Z  trails=0.2h
  1y  pts=  8783  first=2025-07-06T14:14:47Z  last=2026-07-09T10:14:47Z  trails=0.2h
 all  pts= 15249  first=2024-09-30T21:15:11Z  last=2026-07-09T10:14:47Z  trails=0.2h
```

Tolerate a `404` on an individual bucket. A vault younger than a year has no `1y` object, but its `all` and `30d` objects exist. Treat the merge as successful if at least one bucket resolves. Propagate any non-404 failure rather than rendering a partial series without indication.

***

### Endpoints

#### `GET /dapp/plasma-vaults-list`

A compact vault directory (\~100 KB). Use this endpoint to enumerate vaults. It is preferable to `/fusion/vaults`, which is approximately 44 MB.

```jsonc
{
  "timestamp": "2026-07-09T10:01:15.819Z",
  "version": "…",
  "plasmaVaults": [
    {
      "name": "IPOR USDC Lending Optimizer Ethereum",
      "chainId": 1,
      "address": "0x43ee0243ea8cf02f7087d8b16c8d2007cc9c7ca2", // may be MIXED-CASE
      "apr": "3.286746235024654313",
      "totalAssets": "463267371354",                            // raw base units
      "assetAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
      "assetSymbol": "USDC",
      "assetDecimals": 6,
      "marketIds": ["1", "11", "3", "4", "14"],
      "fuses": ["0x465D639EB964158beE11f35E8fc23f704EC936a2", "…"]
    }
  ]
}
```

This is the authoritative list of vaults the API serves. Remember to lowercase `address` before using it in a history path.

***

#### `GET /v2/fusion/vault-history/{chainId}/{vaultAddress}-{period}`

Per-vault time series: APY, TVL, and allocation across markets. **This is the primary charting endpoint.** Subject to the merge rule.

`vaultAddress` **must be lowercase**.

```jsonc
{
  "history": [
    {
      "blockNumber": 25444165,
      "blockTimestamp": "2026-07-02T10:14:47Z",
      "apy": "3.2864762350246543",
      "rewardsApy": "0.104",
      "vestingApy": null,
      "underlyingAssetApy": "3.11",
      "assetsToSharesRatio": "1.0412…",
      "tvl": "490462.27822644341523",       // USD
      "totalBalance": "490648.346799",      // underlying asset units
      "marketBalances": [
        {
          "protocol": "aave-v3",
          "marketId": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
          "balanceType": "DEPOSIT",
          "balance": "11096.690979",
          "balanceUsd": "11092.48278088003383"
        }
      ]
    }
  ]
}
```

`marketBalances` provides the data for the historical-allocation (stacked area) chart: each point records how the vault's assets were distributed across underlying protocols at that block. `apy` is net of fees; `underlyingAssetApy` and `rewardsApy` decompose it.

Some vaults additionally expose `dexPositionBalances` (LP positions, with `token0`/`token1` USD amounts) and `rewardsVestingApy`. Treat all of these as optional and nullable.

***

#### `GET /v2/fusion/market-history/{chainId}/{protocol}/{substrate}-{period}`

Per-market time series: supply and borrow rates and market size for one market on one protocol. Use it to chart the yield of the venues a vault allocates into. Subject to the merge rule.

* `protocol` — a protocol slug, e.g. `aave-v3`.
* `substrate` — identifies the market within the protocol, **lowercase**. For lending pools this is the underlying asset address; for vault-shaped markets (`erc4626`, `meta-morpho`, `morpho-blue`) it is the market or vault address.

```jsonc
{
  "points": [
    {
      "blockNumber": 25444165,
      "timestamp": "2026-07-02T10:14:47Z",
      "supplyApy": "4.1034406162952586",
      "supplyBaseApy": "4.1034406162952586",
      "supplyRewards": [
        {
          "assetAddress": "0xc00e94cb662c3520282e6f5717214004a7f26888",
          "rewardApy": "0.1008130660673186",
          "type": "INSTANT"
        }
      ],
      "borrowApy": "4.9405590449566654",
      "borrowBaseApy": "4.9405590449566654",
      "borrowRewards": [],
      "totalSupply": "2017881149.188354",
      "totalBorrow": "1869968394.182922"
    }
  ]
}
```

`supplyApy` = `supplyBaseApy` + the sum of `supplyRewards[].rewardApy`. Point density is at least hourly and may be finer than the vault history.

Two protocols use a substrate convention that differs from their market id: `spark` markets are keyed by the **DAI address**, and Harvest vaults use the protocol slug `harvest-erc4626`.

***

#### `GET /fusion/vaults`

The full vault and market snapshot, **\~44 MB uncompressed** (\~4 MB gzipped). It embeds a `history` array within every vault object, which accounts for most of the payload size.

```jsonc
{
  "creationDate": "2026-07-09T10:58:00.019119154Z",
  "vaults":  [ { "chainId": 1, "address": "…", "name": "…", "apy": "…",
                 "apr30d": "…", "historicalApr": "…", "history": [ /* … */ ],
                 "tvl": "…", "managedByIporAlpha": false, "alphaDisabled": false } ],
  "markets": [ { "chainId": 1, "protocol": "euler-v2", "marketId": "0xdc1a…",
                 "supplyApy": "…", "supplyBaseApy": "…", "supplyRewards": [],
                 "borrowApy": "…", "supplyApr30d": "…", "borrowApr30d": "…" } ]
}
```

**Do not fetch this endpoint to read a single vault.** Use `/dapp/plasma-vaults-list` for enumeration and the `-history` endpoints for time series. Its principal use is the `markets[]` array, which provides current rates for every market on every chain in a single request. This is the sole reason the IPOR frontend retrieves it.

***

#### `GET /fusion/vaults-customization-list`

Atomist-supplied presentation metadata for vaults that have opted in. Only a minority of vaults appear; absence from this list is normal and not an error.

```jsonc
[
  {
    "chainId": 1,
    "vaultAddress": "0x8f5855D30610b176c972F844B93A28c6D1553416",
    "description": "x*y=k",
    "vaultLogoUrl": "https://api.ipor.io/fusion/vaults-customization/1/0x8f58…/vault-logo",
    "announcements": []
  }
]
```

***

#### `GET /fusion/markets/{chainId}/{protocol}/{marketId}` *(v1)*

The v1 market-history endpoint. Returns market metadata alongside a points array of the same shape as v2:

```jsonc
{ "chainId": 1, "protocol": "aave-v3", "marketId": "0xa0b8…",
  "createdAt": "…", "points": [ /* same shape as market-history */ ] }
```

**v1 is limited to a trailing 30-day window.** It provides no period selector and returns no data older than 30 days.

**Prefer v2 for new integrations.** `/v2/fusion/market-history/…` exposes the full history from vault creation through the `{period}` suffix, up to and including `all`. v1 remains appropriate only where a fixed 30-day window is sufficient and the merge rule is not required.

***

### Supported chains

`chainId` appears in every history path.

| Chain     | `chainId` |
| --------- | --------- |
| Ethereum  | `1`       |
| Arbitrum  | `42161`   |
| Base      | `8453`    |
| Unichain  | `130`     |
| Ink       | `57073`   |
| Plasma    | `9745`    |
| Avalanche | `43114`   |

Chain support expands over time. Rather than hardcoding this table, derive the live set from `GET /dapp/plasma-vaults-list → plasmaVaults[].chainId`.

***

### Protocol slugs

The `{protocol}` path segment takes a slug such as `aave-v3`, `compound-v3`, `morpho-blue`, `meta-morpho`, `euler-v2`, `spark`, `moonwell`, `gearbox-v3`, `fluid-instadapp`, `pendle`, or `erc4626`.

**Do not hardcode the set.** New protocols and yield-bearing assets are added continuously, so any list reproduced here becomes inaccurate over time. Derive it at runtime:

```bash
curl --compressed 'https://api.ipor.io/fusion/vaults' \
  | jq -r '[.markets[].protocol] | unique | .[]'
```

Two slugs follow non-obvious conventions: `spark` markets use the **DAI address** as their substrate, and Harvest vaults use the slug `harvest-erc4626` rather than `erc4626`.

***

### Payload sizes and caching

Orders of magnitude, uncompressed, for a mature Ethereum vault. Payloads grow with vault age and market count, so treat these as a guide to relative cost, not as fixed values:

| Endpoint         | `7d`     | `30d`    | `1y`   | `all`       |
| ---------------- | -------- | -------- | ------ | ----------- |
| `vault-history`  | \~200 KB | \~850 KB | \~8 MB | **\~14 MB** |
| `market-history` | \~100 KB | \~700 KB | \~5 MB | \~6 MB      |

| Endpoint                            | Size                         |
| ----------------------------------- | ---------------------------- |
| `/dapp/plasma-vaults-list`          | \~100 KB (\~20 KB gzipped)   |
| `/fusion/vaults-customization-list` | \~15 KB                      |
| `/fusion/markets/…` (v1)            | \~400 KB                     |
| `/fusion/vaults`                    | **\~44 MB** (\~4 MB gzipped) |

Responses are served from CloudFront and carry `etag` and `last-modified`. Use conditional requests (`If-None-Match`) to avoid re-downloading unchanged objects.

**Cache the long buckets.** An application that re-fetches `-all` at a short interval re-downloads approximately 14 MB to obtain data that is regenerated only rarely. The appropriate polling strategy follows from the freshness table:

* Fetch `all` and `1y` **once** and cache them for hours or days.
* Re-poll **only `7d`** (and `30d`) at your refresh interval. These objects are small and contain all recently added points.
* Re-run the merge against the cached long bucket on each refresh.

Revalidate the cached long buckets periodically with a conditional request rather than caching them indefinitely. Historical data is occasionally recomputed to correct defects, so an `all` object can change without gaining new points. See Data accuracy and corrections.

***

### Reference TypeScript client

Dependency-free. Handles bucket merging, address normalisation, and the XML error case. Verified against the live API.

```ts
/**
 * Minimal client for the IPOR static API (https://api.ipor.io).
 */

const BASE_URL = 'https://api.ipor.io';

export type Period = '7d' | '30d' | '1y' | 'all';

/**
 * Buckets to fetch for a requested period, ordered least-current to most-current.
 * On a duplicate blockNumber the last bucket takes precedence, so the most
 * current bucket must be listed last.
 */
const BUCKETS_TO_MERGE: Record<Period, readonly Period[]> = {
  '7d': ['7d'],
  '30d': ['30d', '7d'],
  '1y': ['1y', '30d', '7d'],
  all: ['all', '30d', '7d'],
};

/** Numeric fields are decimal strings, not numbers — parse with care. */
type Decimal = string;

export interface MarketBalance {
  marketId: string | null;
  protocol: string | null;
  balance: Decimal;
  balanceUsd: Decimal;
  balanceType: string | null;
}

export interface VaultHistoryPoint {
  blockNumber: number;
  blockTimestamp: string; // ISO-8601, e.g. "2026-07-09T09:14:47Z"
  apy: Decimal | null;
  rewardsApy: Decimal | null;
  vestingApy: Decimal | null;
  underlyingAssetApy: Decimal | null;
  assetsToSharesRatio: Decimal | null;
  tvl: Decimal;
  totalBalance: Decimal;
  marketBalances: MarketBalance[];
}

export interface VaultHistoryResponse {
  history: VaultHistoryPoint[];
}

export interface RewardApy {
  assetAddress: string;
  rewardApy: Decimal;
  type: string;
}

export interface MarketHistoryPoint {
  blockNumber: number;
  timestamp: string;
  supplyApy: Decimal | null;
  supplyBaseApy: Decimal | null;
  supplyRewards: RewardApy[];
  borrowApy: Decimal | null;
  borrowBaseApy: Decimal | null;
  borrowRewards: RewardApy[];
  totalSupply: Decimal | null;
  totalBorrow: Decimal | null;
}

export interface MarketHistoryResponse {
  points: MarketHistoryPoint[];
}

/** A missing object. The API answers with an S3 XML body, not JSON. */
export class IporNotFoundError extends Error {
  constructor(readonly path: string) {
    super(`No data at ${path}`);
    this.name = 'IporNotFoundError';
  }
}

async function getJson<T>(path: string, signal?: AbortSignal): Promise<T> {
  const response = await fetch(`${BASE_URL}${path}`, { signal });

  // Unknown vault, unsupported period, or a non-lowercase address all land here.
  if (response.status === 404) throw new IporNotFoundError(path);

  if (!response.ok) {
    throw new Error(`GET ${path} failed: ${response.status}`);
  }

  // Guard against XML slipping through on a non-404 error status.
  const contentType = response.headers.get('content-type') ?? '';
  if (!contentType.includes('application/json')) {
    const body = await response.text();
    throw new Error(`GET ${path} returned ${contentType}: ${body.slice(0, 200)}`);
  }

  return (await response.json()) as T;
}

/** S3 keys are case-sensitive; `plasma-vaults-list` returns mixed-case addresses. */
const normalizeAddress = (address: string): string => address.toLowerCase();

/**
 * Concatenate buckets and sort by blockNumber ascending, retaining the LAST
 * occurrence of any repeated blockNumber. Callers pass buckets least-current
 * first, so an overlapping point is taken from the most current bucket
 * containing it.
 */
function mergeByBlockNumber<T extends { blockNumber: number }>(
  buckets: T[][],
): T[] {
  const byBlock = new Map<number, T>();
  for (const bucket of buckets) {
    for (const point of bucket) byBlock.set(point.blockNumber, point);
  }
  return [...byBlock.values()].sort((a, b) => a.blockNumber - b.blockNumber);
}

/**
 * Fetch every bucket for `period` and merge. Buckets are fetched concurrently;
 * a missing one is tolerated as long as at least one resolves, since not every
 * vault has a full `1y`/`all` history.
 */
async function fetchMerged<
  TPayload extends object,
  T extends { blockNumber: number },
>(
  period: Period,
  buildPath: (bucket: Period) => string,
  extract: (payload: Awaited<TPayload>) => T[],
  signal?: AbortSignal,
): Promise<T[]> {
  const settled = await Promise.allSettled(
    BUCKETS_TO_MERGE[period].map((bucket) =>
      getJson<TPayload>(buildPath(bucket), signal),
    ),
  );

  const rejection = settled.find(
    (r): r is PromiseRejectedResult =>
      r.status === 'rejected' && !(r.reason instanceof IporNotFoundError),
  );
  if (rejection) throw rejection.reason;

  const resolved = settled
    .filter(
      (r): r is PromiseFulfilledResult<Awaited<TPayload>> =>
        r.status === 'fulfilled',
    )
    .map((r) => extract(r.value));

  if (resolved.length === 0) throw new IporNotFoundError(buildPath(period));

  return mergeByBlockNumber(resolved);
}

/**
 * Vault APY / TVL / per-market allocation over time, hourly.
 *
 * The returned series is current to within ~1h regardless of `period`, because
 * the stale tail of the long buckets is backfilled from `30d` and `7d`.
 */
export function fetchVaultHistory(args: {
  chainId: number;
  vaultAddress: string;
  period: Period;
  signal?: AbortSignal;
}): Promise<VaultHistoryPoint[]> {
  const address = normalizeAddress(args.vaultAddress);

  return fetchMerged<VaultHistoryResponse, VaultHistoryPoint>(
    args.period,
    (bucket) => `/v2/fusion/vault-history/${args.chainId}/${address}-${bucket}`,
    (payload) => payload.history,
    args.signal,
  );
}

/**
 * Supply/borrow APY and size for one market, hourly.
 */
export function fetchMarketHistory(args: {
  chainId: number;
  protocol: string;
  substrate: string;
  period: Period;
  signal?: AbortSignal;
}): Promise<MarketHistoryPoint[]> {
  const substrate = normalizeAddress(args.substrate);

  return fetchMerged<MarketHistoryResponse, MarketHistoryPoint>(
    args.period,
    (bucket) =>
      `/v2/fusion/market-history/${args.chainId}/${args.protocol}/${substrate}-${bucket}`,
    (payload) => payload.points,
    args.signal,
  );
}

export interface PlasmaVaultListItem {
  chainId: number;
  /** Mixed-case as returned; lowercase before using it in a history path. */
  address: string;
  name: string;
  apr: Decimal | null;
  totalAssets: Decimal;
  assetAddress: string;
  assetSymbol: string;
  assetDecimals: number;
  marketIds: string[];
}

export interface PlasmaVaultsList {
  timestamp: string;
  version: string;
  plasmaVaults: PlasmaVaultListItem[];
}

/** ~100 KB. Preferred for enumerating vaults; `/fusion/vaults` is ~44 MB. */
export function fetchPlasmaVaultsList(
  signal?: AbortSignal,
): Promise<PlasmaVaultsList> {
  return getJson<PlasmaVaultsList>('/dapp/plasma-vaults-list', signal);
}
```

#### Usage

```ts
// Full lifetime APY series, current to within the hour.
const history = await fetchVaultHistory({
  chainId: 1,
  vaultAddress: '0x43Ee0243eA8CF02f7087d8B16C8D2007cC9c7cA2', // casing handled
  period: 'all',
});

const series = history
  .filter((p) => p.apy !== null)
  .map((p) => ({ t: Date.parse(p.blockTimestamp), apy: Number(p.apy) }));

// Underlying market rates.
const aaveUsdc = await fetchMarketHistory({
  chainId: 1,
  protocol: 'aave-v3',
  substrate: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  period: '30d',
});
```

***

### Data accuracy and corrections

**Vault APY figures are not guaranteed to be exact.** Two causes account for most discrepancies:

* **Strategy changes.** A vault's strategy can be reconfigured over its lifetime. Historical points computed under a previous configuration will not always be directly comparable with later ones.
* **Indexing bugs.** The pipeline that computes APY, TVL, and market balances is complex, and defects occur from time to time.

**Corrections are applied retroactively.** When a bug is identified, the affected history is recomputed and the underlying S3 objects are rewritten. A series you fetched previously may therefore differ from the same series fetched later, and the corrected values are the authoritative ones. If you cache history, treat `etag` and `last-modified` as the signal to re-read rather than assuming historical points are immutable. This applies to the long buckets as well: an `all` object that has not gained new points may still have been rewritten to fix older ones.

**Please report anything that looks wrong.** If you observe an APY, TVL, or allocation figure that does not match your own calculation or on-chain observation, report it in the IPOR Discord:

**<https://discord.com/invite/bSKzq6UMJ3>**

Including the vault address, chain id, period, and the affected timestamps or block numbers will make the issue substantially faster to diagnose. Reports from integrators are a meaningful source of the corrections described above.

***

### Known limitations

**`1y` and `all` are stale by design.** Described in Period buckets and freshness. Without merging, a chart will terminate weeks before the present.

**APY values may be revised.** Historical points are not immutable; see Data accuracy and corrections.

**Overlapping buckets contain duplicate `blockNumber` values.** Deduplicate during the merge. Note that the IPOR frontend's own implementation does not deduplicate. This has no visible effect on a line chart but produces incorrect results for any aggregate computation, so that implementation should not be treated as a reference for numeric work.

**Addresses are case-sensitive in paths but returned checksummed in payloads.** Addresses obtained from the vault list will return 404 if used without normalisation.

**404 responses return XML.** Check `response.status` before parsing the body.

**No pagination, filtering, or date-range selection.** Buckets are retrieved in full. There is no mechanism to request a value between two arbitrary dates; fetch the enclosing bucket and slice the series client-side.

**Two market-history versions coexist.** v1 (`/fusion/markets/…`) is limited to a trailing 30-day window. v2 (`/v2/fusion/market-history/…`) serves the full history from vault creation, selected by the `{period}` suffix. Use v2 unless a 30-day window is sufficient.

**No published SLA or changelog.** This API serves app.ipor.io. It is public and stable in practice, but response shapes may acquire additional fields. Parse defensively: ignore unknown fields, and treat every documented field other than `blockNumber`, `blockTimestamp`/`timestamp`, `tvl`, and `totalBalance` as potentially null.

***

### Provenance

Endpoint inventory derived from the IPOR webapp at `origin/develop` commit `6a734ab9d` — principally `src/config/constants.ts`, `src/fusion/api/vault-history/`, `src/fusion/api/markets/`, and `src/fusion/pages/list/list/usePlasmaVaultsList.ts`. All shapes, sizes, status codes, freshness figures, and the merge behaviour were verified against the live API on **2026-07-09**.

Figures in the freshness and size tables are point measurements against vault `0x43ee0243ea8cf02f7087d8b16c8d2007cc9c7ca2` (Ethereum) and the `aave-v3` / `compound-v3` USDC markets. They characterise the regeneration cadence; they are not guarantees.

Counts that move — how many vaults, markets, protocols, or chains exist — are deliberately omitted from this document. Query them from `/dapp/plasma-vaults-list` and `/fusion/vaults` instead
