# Get Subnets Metagraph History

Per-neuron snapshots over time.

_Source: https://beta.taostats.io/docs/new/subnets/get-subnets-metagraph-history_

_Last reviewed: 2026-10-07_

```http
GET https://api.taostats.io/v1/subnets/metagraph/history
```

Requires an API key in the `Authorization` header.

Per-neuron snapshots over time.

## Try it

Try this request in the browser on the HTML version of this page, or use the code samples below.

### Code samples

**cURL**

```bash
curl -H "Authorization: <YOUR_API_KEY>" \
  "https://api.taostats.io/v1/subnets/metagraph/history"
```

**JavaScript**

```js
const response = await fetch('https://api.taostats.io/v1/subnets/metagraph/history', {
  headers: {
    Authorization: '<YOUR_API_KEY>',
  },
});
const data = await response.json();
```

**Python**

```python
import requests

response = requests.get(
    "https://api.taostats.io/v1/subnets/metagraph/history",
    headers={"Authorization": "<YOUR_API_KEY>"},
)
data = response.json()
```

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `netuid` | query | `integer (int32)` | Yes | Subnet ID (required). |
| `uid` | query | `integer (int32)` |  | Filter by neuron UID. |
| `block_start` | query | `integer (int32)` |  | Block range start (inclusive). |
| `block_end` | query | `integer (int32)` |  | Block range end (inclusive). |
| `timestamp_start` | query | `integer (int64)` |  | Timestamp range start, Unix seconds (inclusive). |
| `timestamp_end` | query | `integer (int64)` |  | Timestamp range end, Unix seconds (inclusive). |
| `has_incentive` | query | `boolean` |  | Keep only neurons with positive incentive (`false`: only neurons with none). The same test as `/v1/subnets/metagraph`'s `has_incentive`: the served, weighted `incentive`. |
| `page` | query | `integer (int32)` |  | Page number (1-indexed). Default: 1. Bounded window: `(page − 1) × limit` must be below 1,000,000. Deep `OFFSET` paging is linear in the offset, so an uncapped `page` lets one request scan tens of millions of rows; a page past the window is rejected with a 400. Reach deeper history with this endpoint's range filters (`block_start`/`block_end`, `timestamp_start`/`timestamp_end`) rather than a larger `page`. |
| `limit` | query | `integer (int32)` |  | Results per page. Default: 50. Max: 200. |
| `order_by` | query | `string` |  | Column to order by. Default: `timestamp`. One of `timestamp`. |
| `order_dir` | query | `string` |  | Sort direction. Default: `desc`. One of `asc`, `desc`. |

## Responses

### `200` — Per-neuron metagraph snapshots over time

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | `array` | Yes |  |
| `data[].active` | `boolean` | Yes | Whether the neuron is active. |
| `data[].alpha_stake` | `string` | Yes | Alpha stake (RAO, `u64` decimal string): the chain's inherited alpha stake at the row's block, the snapshot row's `alpha_stake`. |
| `data[].axon` | `string, nullable` | Yes | Axon `ip:port` (`null` when no axon is set). |
| `data[].block_number` | `integer (int32)` | Yes | Block height of the snapshot. |
| `data[].coldkey` | `string` | Yes | Coldkey (SS58). |
| `data[].collateral_earned_alpha` | `string` | Yes | Emission earned while this collateral entry has existed (alpha-RAO decimal string) — the progress meter against the lock, since release is `drain_ratio × emission`. Not a lifetime-of-hotkey total. |
| `data[].consensus` | `string` | Yes | Consensus score (normalised `u16 / 65535`). |
| `data[].daily_burned_alpha` | `string, nullable` | Yes | Daily burned alpha (RAO decimal string; `null` when not applicable). |
| `data[].daily_burned_alpha_as_tao` | `string, nullable` | Yes | Daily burned alpha as TAO (RAO decimal string; `null` when not applicable). |
| `data[].daily_mining_alpha` | `string, nullable` | Yes | Daily mining alpha (RAO decimal string; `null` when not applicable). |
| `data[].daily_mining_alpha_as_tao` | `string, nullable` | Yes | Daily mining alpha as TAO (RAO decimal string; `null` when not applicable). |
| `data[].daily_mining_tao` | `string, nullable` | Yes | Daily mining TAO (RAO decimal string; `null` when not applicable). |
| `data[].daily_owner_alpha` | `string, nullable` | Yes | Daily owner alpha (RAO decimal string; `null` when not applicable). |
| `data[].daily_owner_alpha_as_tao` | `string, nullable` | Yes | Daily owner alpha as TAO (RAO decimal string; `null` when not applicable). |
| `data[].daily_total_rewards_as_tao` | `string, nullable` | Yes | Daily total rewards as TAO (RAO decimal string; always present). |
| `data[].daily_validating_alpha` | `string, nullable` | Yes | Daily validating alpha (RAO decimal string; `null` when not applicable). |
| `data[].daily_validating_alpha_as_tao` | `string, nullable` | Yes | Daily validating alpha as TAO (RAO decimal string; `null` when not applicable). |
| `data[].daily_validating_tao` | `string, nullable` | Yes | Daily validating TAO (RAO decimal string; `null` when not applicable). |
| `data[].dividends` | `string` | Yes | Dividends (normalised `u16 / 65535`). |
| `data[].emission` | `string` | Yes | Emission (RAO, `u64` decimal string). |
| `data[].free_alpha` | `string` | Yes | The hotkey's own alpha not frozen by collateral: `hotkey_alpha - locked_alpha`, floored at `"0"` (alpha-RAO decimal string). Read this, not `alpha_stake`, as "withdrawable at this UID". Per-*position* (coldkey, hotkey, netuid) withdrawable lives on `/v1/alpha/*`. |
| `data[].hotkey` | `string` | Yes | Hotkey (SS58). |
| `data[].hotkey_alpha` | `string` | Yes | The hotkey's **own** alpha on this subnet (alpha-RAO decimal string) — `TotalHotkeyAlpha(hotkey, netuid)`. Not the same quantity as `alpha_stake` above, which is the chain's *inherited* stake (own − delegated to children + inherited from parents) after the family-parity adjustment. Collateral is locked inside this pool, so this is the base the split below is taken from: `locked_alpha + free_alpha == hotkey_alpha` (see `locked_alpha` for the one flooring exception). `"0"` on rows written before these columns existed — the value was never recorded there, and `"0"` on both base and free is what says so. |
| `data[].in_danger` | `boolean` | Yes | Whether the neuron is in danger of deregistration. |
| `data[].incentive` | `string` | Yes | Incentive: recall's split-weighted incentive across the subnet's mechanisms, normalised to `0..=1` (#1068); the plain `u16 / 65535` on a single-mechanism subnet. |
| `data[].is_child_key` | `boolean` | Yes | Whether this hotkey is a child key on this subnet. |
| `data[].is_immune` | `boolean` | Yes | Whether the neuron is within its immunity period. |
| `data[].is_owner_hotkey` | `boolean, nullable` | Yes | Whether this is the subnet owner's hotkey. Derived from the snapshot's projected `daily_owner_alpha`, which the indexer sets **iff** the neuron's hotkey owns the subnet. `null` on rows written before the projected fields landed (#345), where owner state is genuinely unknown rather than false. |
| `data[].locked_alpha` | `string` | Yes | Alpha still locked as v435 registration collateral (alpha-RAO decimal string). **Part of** `hotkey_alpha`, not additional to it — the lock is a flag on real stake. One chain-side exception: both figures floor independently through the share pool, so a long-bonded position can carry a lock a few rao above its own pool, and `free_alpha` floors at `"0"` there rather than the sum holding exactly. `"0"` for every neuron whose hotkey holds no collateral. Registration locks it only on a subnet with a nonzero `CollateralLockShare`, but a voluntary `add_collateral` can lock it on any subnet. |
| `data[].mech_incentive` | `array` | Yes | Per-mechanism incentive (normalised `u16 / 65535`, one per mechanism). |
| `data[].mech_incentive[]` | `string` | Yes |  |
| `data[].mech_updated` | `array` | Yes | Blocks since each mechanism's last update (one element per mechanism). |
| `data[].mech_updated[]` | `integer (int32)` | Yes |  |
| `data[].min_locked_alpha` | `string` | Yes | The miner-set collateral floor (alpha-RAO decimal string). The drain never releases `locked_alpha` below it, and below it the chain captures earned incentive into the lock instead of paying it out. `"0"` when no floor is set. |
| `data[].miner_rank` | `integer (int32), nullable` | Yes | Miner rank among positive-incentive neurons (`null` otherwise). Equal incentives share a rank and the next value skips (1, 1, 3), matching recall's `rankBy`. |
| `data[].name` | `string, nullable` | Yes | Identity name (from the coldkey's on-chain identity). |
| `data[].netuid` | `integer (int32)` | Yes | Subnet ID. |
| `data[].registered_at_block` | `integer (int32)` | Yes | Block at which the neuron registered. |
| `data[].root_stake` | `string` | Yes | Root stake (RAO, `u64` decimal string): the snapshot row's `tao_stake`. |
| `data[].root_stake_as_alpha` | `string` | Yes | Root stake as alpha (RAO, `u64` decimal string): `total_alpha_stake - alpha_stake`. |
| `data[].root_weight` | `string` | Yes | Root weight (decimal string; family-derived, `"0"` if no family row). |
| `data[].stake_weight` | `string, nullable` | Yes | Stake weight (normalised `u16 / 65535`). |
| `data[].timestamp` | `string` | Yes | ISO 8601 timestamp with millisecond precision. |
| `data[].total_alpha_stake` | `string` | Yes | Total alpha stake (RAO, `u64` decimal string): the snapshot row's `total_stake`. |
| `data[].uid` | `integer (int32)` | Yes | Neuron UID. |
| `data[].updated` | `integer (int32)` | Yes | Blocks since the neuron's last update (`block_number - last_update`). |
| `data[].validator_permit` | `boolean` | Yes | Whether the neuron holds a validator permit. |
| `data[].validator_rank` | `integer (int32), nullable` | Yes | Validator rank (among positive-dividend neurons; `null` otherwise). |
| `data[].validator_trust` | `string` | Yes | Validator trust (normalised `u16 / 65535`). |
| `pagination` | `object` | Yes | Pagination metadata appended to collection responses. `next_page` / `prev_page` are explicitly required+nullable in the OpenAPI spec: per `docs/api_standards.md` we emit `null` rather than omitting the field, so the SDK should model them as `Option`/nullable types that are always present in the response. **`total_items` is the exact number of matching rows** on every route (issue #992). It used to stop at 1,000,000, which reported a million transfers when finney holds 7,513,252 and a million events when the real figure is 981,179,110; the cap was removed on 29 Sep 2026 and nothing may reintroduce one — `crate::pagination::exact_count_sql` is the only count form, and `no_capped_count_reaches_the_api_surface` in `data-debug-scripts` fails the build if a capped one comes back. **`total_pages` is still bounded**, to `crate::pagination::max_reachable_page(per_page)`, because the `page` parameter is capped to a 1,000,000-row window: deep `OFFSET` paging reads every skipped row (24.7M rows / 36.6 GiB / 42 s measured on one endpoint, issue #413). So `total_pages` is exactly the last page this API will serve and `next_page` / `prev_page` stay coherent at the window edge, while `total_items` tells the caller how many rows there really are. History beyond the window is reached with each endpoint's range filters, not a larger `page`. |
| `pagination.current_page` | `integer (int32)` | Yes | 1-indexed page that produced the rows in `data`. |
| `pagination.next_page` | `integer (int32), nullable` | Yes | Next page number, or `null` if this is the last page. |
| `pagination.per_page` | `integer (int32)` | Yes | Number of items requested per page. |
| `pagination.prev_page` | `integer (int32), nullable` | Yes | Previous page number, or `null` if this is the first page. |
| `pagination.total_items` | `integer (int64)` | Yes | Number of items matching the query across all pages. This is the exact count — every matching row, however many there are. It is not bounded by `total_pages`, so a listing can legitimately report far more items than `total_pages × per_page`. |
| `pagination.total_pages` | `integer (int32)` | Yes | Number of pages at the current `per_page` that this API will serve: `ceil(total_items / per_page)`, capped at `ceil(1000000 / per_page)` because the `page` parameter only reaches into the first 1,000,000 rows. When the cap bites, `total_items` is still the true total and this is the last page a request can ask for; reach deeper history with the endpoint's range filters instead of a larger `page`. |

### `400` — Invalid query parameter

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `message` | `string` | Yes | Plain-english message describing the failure from the caller's perspective. |
| `status_code` | `integer (int32)` | Yes | HTTP status code (mirrored in the response status line for convenience). |

### `500` — Internal server error

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `message` | `string` | Yes | Plain-english message describing the failure from the caller's perspective. |
| `status_code` | `integer (int32)` | Yes | HTTP status code (mirrored in the response status line for convenience). |

Every request needs an `Authorization` header holding your API key — see [Getting started with the Taostats API](https://beta.taostats.io/docs/start-here/getting-started-with-taostats-api).
