<!-- Published copy of the ValidatorFinances API handbook. Source of truth: the API repository, revision 1b5ecf0 (2026-07-17). Published on art3mis.cloud 2026-09-08. -->

> **Canonical URL:** https://art3mis.cloud/api/validator-finances.md
> **API base URL:** https://trent.art3mis.cloud/fin (all paths below are relative to it)
> **Validator covered:** identity GwHH8ciFhR8vejWCqmg8FWZUCNtubPY2esALvy5tBvji, vote account 3iPuTgpWaaC6jYEY7kd993QBthGsQTK3yPCrNJyPMhCD
> **Operator:** ART3MIS.CLOUD Limited (BVI 2195776), contact a3mc@protonmail.com
> **Terms:** public, unauthenticated, read-only, JSON, CORS open, fair use; the CDN in front of the API applies rate limits.
> **Contract:** this document is the API contract; if something is not in it, it is not part of the API.
> **Revision:** source 1b5ecf0 (2026-07-17). Audited against production on 2026-09-08 (epoch 1031): 40 behavioural checks over every endpoint, envelope, row shape, unit typing and validation rule; two statements corrected to match production (reconstructed `/apy` rows also carry the net delegator pair; `/payers/{pubkey}/txs` does not validate the pubkey format).

# validator-finances API — agent skill

Base URL: `https://trent.art3mis.cloud/fin`. All endpoints are `GET`, JSON,
read-only, unauthenticated, CORS open. Errors from the API are always
`{"error": "<reason>"}` with HTTP 400/404/500; a plain-text 5xx page comes
from the CDN in front of the API, not from the API itself.

## 1. What this system is and why it exists

A daemon runs beside a high-performance Solana mainnet validator. It
tracks the validator's leader schedule, crawls **every block the
validator produces**, decomposes each block's revenue at the transaction
level, closes a per-epoch income ledger, values everything in USD at
block time, stores every cluster validator's per-epoch results, and
serves the aggregates over this API.

It exists to answer the operator's real questions:

- **"How much did we actually make?"** — net profit per epoch/day/range,
  with every income channel and expense separated (never a blended
  number the operator must decompose by hand).
- **"What is our APY?"** — institutions ask constantly, in two distinct
  senses (see §2.4), and always versus the cluster.
- **"Who pays us and for what?"** — fee-payer and program attribution
  per block, for scheduler strategy.
- **"When is our next block?"** — schedule with a countdown.
- **"Should we change our MEV commission?"** — counterfactual revenue at
  any hypothetical commission over the full settled history.
- **"Is anything wrong?"** — skip/abandon counters, fee-model self-audit,
  price coverage.

## 2. Core semantics — read before using any number

### 2.1 Units and types

| Convention | Rule |
| --- | --- |
| SOL amounts | integer **lamports**; 1 SOL = 1,000,000,000 lamports. Fields end `_lamports`. |
| USD amounts | integer **micro-USD**; $1 = 1,000,000. Fields end `_usd_micro`. |
| Prices | `price_usd_micro` = micro-USD per whole SOL (82_200_000 = $82.20/SOL). |
| Time | unix **seconds** UTC (`block_time`, `t`, `start_time`, `end_time`). |
| Epoch | ~2 days, 432,000 slots, ~400 ms per slot (~2.5 slots/s). |
| Big integers | values that can exceed 2^53 arrive as **JSON strings** and must be parsed as 64-bit integers: `cluster_stake`, `activated_stake`, `jitosol_pool_lamports`, `jitosol_pool_supply`. Everything else is a JSON number ≤ 2^53. |
| Percentages | already scaled (`5.57166` means 5.57166%). |

### 2.2 NULL is never zero (the honesty invariant)

`null` means **unknown or unavailable**; `0` means **measured zero**.
The system never fabricates zeros: an unpriced block has
`price_usd_micro: null`, an epoch whose delegator sample failed has
`delegator_apr_pct: null` (it heals later), a pre-BAM epoch has BAM
fields `null`. Two kinds of null coexist: **transient** ones self-heal
as data settles (an unsettled `delegator_apr_pct`, an unpriced block, a
not-yet-settled MEV epoch); **permanent** ones mean the ground truth
never existed and cannot be reconstructed (operator `apr_stake_pct` /
`apy_stake_pct` and the cluster stake-weighted columns for an epoch with
no boundary-capture stake — §2.7). Both render as absence, never zero;
do not wait for a permanent null to fill. When aggregating, skip nulls —
never coerce to 0 — and report coverage using the provided counters
(`priced_blocks`, `mev_covered`, `settled_epochs`, …). A USD sum over a
range covers only its `priced_blocks`; say so when presenting it.

### 2.3 The fee model and the ground-truth self-audit

Per block: `base_fee = 5000 lamports x signatures` (including precompile
signatures), half of base is burned, and the leader keeps
`base/2 + priority fees`. The ledger's own per-block reward entry is
**ground truth**; the daemon's transaction-level decomposition is
analytics on top. `decomposition_delta_lamports` is their difference
and is **0 on every block in normal operation** — any nonzero value
means the on-chain fee rules drifted from the model and the analytics
(not the ground-truth totals) need review. Surface nonzero deltas
prominently.

Tips (Jito MEV) are **not fees**: they are transfers to tip accounts,
tracked separately (`tips_lamports`), and at this validator's 0 bps MEV
commission they flow to stakers, not the operator (see §2.5).

### 2.4 Two different "APY"s — never confuse them

- **Operator view** (`apr_stake_pct` / `apy_stake_pct` in `/apy`): the
  operator's net block-revenue profit annualized against total activated
  stake. Small by construction (~0.1–0.3%); it measures operational
  profitability, not what anyone earns on stake. The stake basis is the
  epoch-boundary capture solely (§2.7), so both fields are `null` for any
  epoch without a capture.
- **Delegator view** (`delegator_apr_pct` / `delegator_apy_pct`): what a
  delegator's stake earns (inflation rewards measured from a real
  reference stake account, plus MEV share). This is the number
  institutions mean by "your APY" (~5–8% territory).

Cluster comparison columns in `/cluster` are delegator-view.

### 2.5 Income channels (who actually receives what)

| Channel | Field(s) | Recipient at this validator's settings |
| --- | --- | --- |
| Block fees (base kept + priority) | `fee_reward_lamports`, ledger `block_fees_lamports` | operator |
| Tips through blocks | `tips_lamports`, `tips_gross_kobe_lamports`, `/mev` `tips_flow_lamports` | stakers (0 bps MEV commission); operator's hypothetical cut = `/mev` counterfactual |
| BAM boost | `bam_boost_entitled_jitosol` (JitoSOL), `bam_sol_lamports` (valued) | operator, commission-independent — the operator's actual Jito income |
| Inflation commission | `inflation_commission_lamports` | 0 (0% vote commission) — structural zero, not unknown |
| Vote cost, operator txs | `vote_cost_lamports`, `operator_tx_cost_lamports` | expenses |

Net profit per epoch = block fees + BAM (+ MEV claim, structurally 0)
− vote cost − operator tx cost = `/apy` `net_total_lamports`.

Two tip figures coexist by design: block-observed
(`tips_gross_lamports`) and Kobe-settled (`tips_gross_kobe_lamports`);
they differ a few percent and are both stored, never reconciled.

### 2.6 Data freshness model

The daemon polls every ~60 s. Blocks appear minutes after production;
prices attach on quiet ticks (Binance 1-minute, CoinGecko daily
fallback, `price_source` labels which); a whole-cluster stake +
commission boundary capture lands in the first slots of each epoch; the
income ledger for epoch N closes after N ends and every leader slot is
resolved and costs are enumerated (minutes to a few hours after the
roll); the measured applied-commission sweep (`getInflationReward`)
fills in after each epoch closes; rank/cluster rows land once per epoch;
MEV settlement (Kobe) lags ~1 epoch. Downtime delays data, never loses
it — gaps self-heal, except the permanent nulls of §2.2/§2.7, which
depend on a capture the engine can only take live, in the epoch itself.

### 2.7 How stake and commission are determined

Every stake- and commission-based number rests on two values the engine
treats as measured ground truth, never inferred. Understanding how they
are obtained explains why some of them are `null`.

**Applied commission.** The commission a validator advertises today is
not necessarily the commission a past epoch's rewards paid: Solana fixes
the commission that applies to an epoch's rewards ahead of time, so the
current rate can differ from what an earlier epoch actually charged. The
engine therefore reads the commission the rewards *actually paid* from
the reward record itself (`getInflationReward`), per epoch, for this
validator and for every validator in the cluster. `/cluster` scores on
that measured value and reports how much of the cluster it had a
measurement for (`commission_measured` vs `commission_missing`); a
validator whose commission is not measured for an epoch is left out of
that epoch's stake-weighted score rather than scored on its current
rate. This is why the score reflects what stakers actually kept, not an
advertised number.

**Activated stake.** A validator's activated stake is constant within an
epoch but changes at every boundary, and the chain exposes only the
*current* value — there is no query for a past epoch's stake. The engine
captures every validator's stake once, in the first slots of each epoch,
and uses that captured value as the exact basis for all stake-relative
math: operator `apr_stake_pct` / `apy_stake_pct` (`/apy`), the
stake-weighted columns (`/cluster`), and the inflation-flow basis
(`/commission`). An epoch that was never captured — anything before
tracking began, or a gap — has no exact stake and reports `null` for
these fields. It is never filled in with the current or an approximate
stake: dividing an epoch's income by a different epoch's stake would
misstate the rate, and an honest gap is worth more than a wrong yield.

The same boundary capture also records each validator's current
commission as a forecast of what the *next* epoch's rewards will pay —
a forward-looking prediction held alongside the measured value, never
merged into it. For a closed epoch, the measured commission is always
the authority.

The consequence for the earliest tracked epochs: their operator APR is
permanently `null` (no capture was taken, and the stake cannot be
recovered afterward), while their delegator APY — a per-lamport rate, so
the stake basis cancels — and their fee and MEV decomposition stay
exact. A gap in one number does not taint the others.

## 3. Endpoint reference

Query params by endpoint family. Block-range endpoints (`/blocks`, `/summary`,
`/earnings`, `/payers/top`, `/programs/top`) take `limit` (clamped
server-side), `from_slot/to_slot`, `from_time/to_time` (unix s), `hours` (trailing window, 1–8760: sets
`from_time = now − hours×3600` from the server clock, UTC; an explicit
`from_time` wins over it), `after` (pagination cursor: last slot of the
previous page). Epoch-ledger endpoints (`/income`, `/apy`, `/rank`,
`/cluster`, `/skips`, `/commission`, `/mev`) take only `from_epoch/to_epoch`
(span clamped to 1000) plus their own documented options; `limit` is
rejected there with 400. Invalid
params return 400 with an explanatory `error` (e.g. `bucket "year":
expected epoch | day | hour`). Unknown or misspelled params are rejected
with 400 — a typo can never silently serve whole-history data as if the
filter applied.

Response shapes — three conventions:

- **Bare array** of row objects: `/skips`, `/income`, `/apy`, `/rank`,
  `/cluster`.
- **`{"range": {...}, "rows": ...}`** (range = the resolved slot/time
  bounds): `/blocks`, `/summary`, `/earnings`, `/payers/top`,
  `/programs/top`. `rows` is an array except in `/summary`, where it is
  a single totals object.
- **Plain object**: `/health`, `/schedule`, `/blocks/{slot}`, `/mev`,
  `/blocks/{slot}/txs`, `/payers/{pubkey}/txs` (§3.14).

### 3.1 `/health` — liveness and progress gauges

```
GET https://trent.art3mis.cloud/fin/health
{"schema_version":20,"blocks_total":388,"latest_block_slot":431706019,
 "latest_block_time":1783561511,"leader_slots_pending":268,
 "leader_slots_produced":388,"leader_slots_skipped":0,
 "leader_slots_abandoned":0,"latest_ledger_epoch":998,"latest_rank_epoch":998,
 "chain_head_age_secs":11,"unpriced_blocks":0}
```

`leader_slots_pending` draining toward 0 = backfill progressing.
`leader_slots_abandoned` is an **ingest-completeness** gauge, not a
Solana concept. It counts leader slots the daemon could not crawl after
exhausting its retry budget on a slot-specific failure (the RPC proved
the slot unavailable, returned a node error, or the response would not
parse); transport/network failures retry forever and never abandon. It
means *the engine is missing that block's data* — it does **not** mean
the validator failed to produce, which is `leader_slots_skipped` (the
on-chain outcome). A nonzero value blocks that epoch's income ledger
rather than feed it a partial sum, and slots still within the backfill
horizon auto-revive and retry on the next epoch roll — so a small
transient count is normal; a persistent one means ingest is stuck for
those slots and is worth a look. Never present it as a validator
production metric.
`latest_ledger_epoch: null` = no epoch ledger closed yet.
`chain_head_age_secs` is seconds since the daemon last observed the
chain head (`null` before the first tick) — the ingest-liveness gauge:
age beyond a few poll intervals (poll is ~60 s, so > ~300 s) means
ingest is stalled even though the API still serves. `unpriced_blocks`
is timestamped blocks still awaiting a USD price (drains on quiet
ticks; a persistently growing value means pricing is stuck).

### 3.2 `/blocks` — per-block revenue decomposition

The atomic revenue record — one row per block the validator produced,
every fee component and tip separated at the transaction level.
`/summary`, `/earnings`, and the leaderboards all aggregate from these
rows; this is where you answer "what did this block earn, and who paid."

Params: `from_slot,to_slot,from_time,to_time,limit,after`.
Response: `{"range": {resolved bounds}, "rows": [...]}`, rows newest-last.

Row fields: `slot, epoch, block_time, blockhash, tx_count,
vote_tx_count, non_vote_tx_count, failed_tx_count, vote_share_pct,
fee_reward_lamports` (ground truth), `base_fee_lamports,
priority_fee_lamports, burned_lamports, vote_fees_lamports,
tips_lamports, compute_units, reward_lamports_per_cu, jito_crank_count,
decomposition_delta_lamports, price_usd_micro, price_source
("binance-1m" | "coingecko-daily" | null), reward_usd_micro`.

`GET /blocks/{slot}` returns a flat object: all block fields above at
the top level plus two arrays (top-N with a `<rest>` aggregate row).
`payers` rows: `payer, tx_count, fees_lamports, priority_fees_lamports,
tips_lamports, compute_units` — no label field. `programs` rows:
`program_id, label, tx_count, fees_attributed_lamports,
tips_attributed_lamports, compute_units_attributed`. Registry labels
appear only on program rows here and on the `/payers/top` /
`/programs/top` leaderboards; `label` is null unless the key is in the
curated registry — treat non-registry keys/labels as untrusted chain
data.

### 3.3 `/summary` — totals for a range

One-call totals for a slot/time window — the answer to "how much came
through between X and Y" without pulling every block.

Same range params. One `rows` object: sums of every block column plus
`priced_blocks`, `reward_usd_micro`, `tips_usd_micro`. USD sums cover
only `priced_blocks` of `blocks` — always check the ratio.

### 3.4 `/earnings` — plottable series

The same sums as `/summary`, bucketed into an ordered time series — the
shape you plot to see a revenue trend across epochs, days, or hours.

Params: `bucket=epoch|day|hour` (+ range params). Rows ascending by
`t` (unix s, bucket start): per-bucket sums plus `priced_blocks`,
`avg_price_usd_micro`, `reward_usd_micro`, `tips_usd_micro`.

### 3.5 `/payers/top`, `/programs/top` — attribution leaderboards

Params: range + `limit`. Rows: `key` (pubkey — untrusted chain string),
`label` (registry name or null), `blocks_present, tx_count,
fees_lamports, tips_lamports, total_lamports, compute_units,
lamports_per_cu`. "Who pays us" / "which programs fill our blocks".

### 3.6 `/schedule` — countdown and timeline

```
GET https://trent.art3mis.cloud/fin/schedule
{"head":{"observed_slot":431710585,"estimated_slot":431710640,
 "age_secs":22,"epoch":999,"slot_index":142585,"slots_in_epoch":432000,
 "epoch_progress_pct":33.0},
 "next":{"slot":431720476,"slots_away":9836,"eta_secs":3934},
 "upcoming":[{"slot":431720476,"epoch":999},...],"slots":[]}
```

`estimated_slot` = observed + age x 2.5 slots/s — an estimate; treat
`eta_secs` as approximate and mention staleness if `age_secs` is large
(> ~300 s). `next: null` = nothing scheduled ahead. With `?epoch=N` the
`slots` array holds the epoch's full timeline: `{slot, epoch, status
(produced|skipped|scheduled|unavailable|abandoned), block_time,
fee_reward_lamports}` — one call renders a schedule plot. `produced` and
`skipped` are the on-chain outcomes; `scheduled`, `unavailable`, and
`abandoned` are the engine's crawl lifecycle for a slot it has not (yet)
fetched, not chain states (see §3.1 on `abandoned`).

### 3.7 `/skips` — per-epoch production quality

Block-production reliability per epoch: of the leader slots the validator
was assigned (`total`), how many became blocks (`produced`) versus were
missed (`skipped`), with `skip_rate_pct` the missed fraction. A rising
skip rate is lost revenue and a health signal; `unavailable` and
`abandoned` are slots the engine could not resolve, not confirmed misses.

Rows per epoch: `total, pending, produced, skipped, unavailable,
abandoned, skip_rate_pct` (null until the epoch has resolved slots).

### 3.8 `/income` — the raw epoch ledger

Rows per closed epoch: `epoch, start_slot, start_time, end_time,
block_fees_lamports, tips_gross_lamports, tips_gross_kobe_lamports,
mev_claimed_lamports, inflation_commission_lamports,
bam_boost_entitled_jitosol, jitosol_pool_lamports (string),
jitosol_pool_supply (string), vote_cost_lamports, vote_tx_count,
vote_fail_count, operator_tx_cost_lamports, operator_tx_count,
mev_commission_bps, blocks, priced_blocks, block_fees_usd_micro`.
Activated stake is not part of the raw ledger — it
is an epoch-boundary measurement (§2.7), served on `/apy` where the
yields that depend on it live. BAM SOL value =
`jitosol x pool_lamports / pool_supply`.

**Receipt-time USD valuation.** Price exists only at the moment of
receipt — there is no per-epoch price. `block_fees_usd_micro` is the
sum of per-block `fee x that block's earn-time price` over the epoch's
priced blocks (the identical per-block valuation `/summary`'s
`reward_usd_micro` sums), `null` when the epoch has zero priced blocks —
never a boundary or average price, never 0. Coverage: the sum covers
exactly `priced_blocks` of `blocks` (same convention as §3.3); always
check the ratio before presenting the figure, and say when it is
partial. An unpriced block contributes absence, not a zero term, so a
partial sum understates rather than misstates — the counters are how
you disclose that.

No other ledger channel has a USD field, deliberately:

- `vote_cost_lamports` / `operator_tx_cost_lamports` are stored as
  epoch aggregates only — the per-transaction receipt times are not
  retained (votes land in other validators' blocks, which this system
  does not price), so no per-receipt valuation is possible.
- `mev_claimed_lamports` and BAM boost have no captured receipt
  timestamp (Kobe settlement and claim times are not recorded).

An aggregate valued at any single price would be an approximation
presented as a measurement; the API serves nothing instead. Convert at
your own peril — and label any such conversion as an estimate, never as
this system's data.

### 3.9 `/apy` — the yield report

Rows per closed epoch: `epoch, duration_secs, net_total_lamports,
bam_sol_lamports, vote_cost, operator_tx_cost, tips_gross,
activated_stake (string), apr_stake_pct, apy_stake_pct,
delegator_apr_pct, delegator_apy_pct, delegator_apr_inflation_pct,
delegator_apy_inflation_pct, stake_source, row_provenance`. See §2.4 for which "APY" is which.
`delegator_*: null` self-heals when the reference-account sample lands; the NET columns
(`delegator_apr_pct`, `delegator_apy_pct`) additionally stay `null` until MEV settlement — the BASE columns (`delegator_apr_inflation_pct`, `delegator_apy_inflation_pct`)
serve immediately at epoch close and never wait on Kobe.
`activated_stake` and the operator `apr_stake_pct` / `apy_stake_pct`
come from the epoch-boundary stake capture (§2.7), with a validated fallback
for epochs that have no capture; both are `null` only when
neither source has the epoch. The delegator APR/APY is a per-lamport rate
(the stake basis cancels), so it stays populated regardless of stake
provenance. `stake_source` names which source served `activated_stake`:
`"capture"` = our own epoch-boundary capture; `"validator_history"` = the
on-chain Jito StakeNet archive, imported only after it matched our own
captures lamport-exact (`backfill-stake`); `null` when neither source has
the epoch. `delegator_ap[ry]_pct` is
the NET rate (inflation plus MEV share) — realized yield;
`delegator_ap[ry]_inflation_pct` is the same basis with the MEV/tips
addend removed — the BASE rate, useful for quality comparison because a
single bursty MEV epoch cannot inflate it.
`row_provenance`: `"tracked"` for every epoch backed by our own crawl
(epoch ≥ 997 today); `"reconstructed"` for epochs below the fixed 997
backfill cutover, served from an accepted multi-account anchor-rate
reconstruction joined to measured epoch durations — same cutover and same
acceptance gate `/cluster`'s backfill arm already uses. A `"reconstructed"` row carries
`epoch`, `duration_secs`, the BASE pair `delegator_apr_inflation_pct` /
`delegator_apy_inflation_pct`, and the NET pair `delegator_apr_pct` /
`delegator_apy_pct` (computed with the identical
annualization/compounding/rounding as the tracked arm; verified against
production 2026-09-08, epochs 899–996); the operator columns, `activated_stake`,
`stake_source`, costs, tips and BAM are `null` on those rows, never a
fabricated or borrowed value.

### 3.10 `/rank` — vote-credit standing

Rows per epoch: `epoch, credits_earned, rank` (1 = best, competition
ranking), `validators, percentile, top_credits, top_vote_pubkey,
gap_to_top, behind_top_pct`. Typical healthy values: rank in the top
tens of ~700–1000, `behind_top_pct` in thousandths of a percent.

### 3.11 `/cluster` — the average everyone asks for

Params: `from_epoch,to_epoch`. Rows per epoch: `validators,
cluster_stake (string), avg_credits, median_credits, top_credits,
avg_score, stake_weighted_score` (score = credits x (1 − commission)),
`cluster_avg_delegator_apr_pct, cluster_stake_weighted_apr_pct`
(inflation-only, derived by scaling this validator's measured rate by
score ratios), `mev_covered, cluster_mev_apr_pct` (coverage-gated:
null until covered stake ≥ half the cluster), `cluster_total_apr_pct`
(inflation + MEV), `commission_measured`, `commission_missing`,
`provenance` (`"live"` or `"backfill"`, the fixed historical
cutover at epoch 997 — every row is one or the other, no partial rows;
`"backfill"` rows have no data yet, Stage 1 ships the column empty of
history). Compare `/apy` `delegator_apr_pct` against these for the "you vs
cluster" answer.
Note: this column stays the fixed epoch-cutover label even though the
anchor rate feeding `cluster_avg_delegator_apr_pct` /
`cluster_stake_weighted_apr_pct` for a `"live"`-provenance epoch can itself
be sourced internally from a healed reconstruction; that finer provenance
is not surfaced here.
Provenance (why some columns narrow): the stake-weighted columns
(`cluster_stake`, `stake_weighted_score`, the MEV-stake terms) use the
epoch-boundary stake capture, so a validator without a capture that epoch
sits out that math; commission enters the score only where it was measured
(`commission_measured` counted, `commission_missing` left out) — see §2.7.
The unweighted credit metrics (`validators`, `avg/median/top_credits`) cover
every validator with exact credits, so they are the widest, most complete
columns.

Distribution columns (for the yield chart's median line and top-decile
band): `delegator_apy_p50_pct` (network median), `delegator_apy_p90_pct`
and `delegator_apy_max_pct` (band edges), `distribution_validators` (how
many validators entered). Each validator's point is its estimated net
delegator APY — the measured anchor rate scaled by score ratio, plus its
staker MEV share over its capture stake — compounded exactly like `/apy`.
A validator with an unmeasured commission, or settled MEV rewards with an
unknown rate or no capture stake, is excluded, never approximated; all
four columns are null for epochs with no computable distribution. Plot
our `/apy` `delegator_apy_pct` against `p50` and the `p90`-`max` band —
all three series are in the same compounded-APY units.

Base (inflation-only) distribution columns:
`delegator_apy_infl_p50_pct`, `delegator_apy_infl_p90_pct`,
`delegator_apy_infl_max_pct`, `distribution_infl_validators` — same
construction, MEV excluded, so a single validator's bursty MEV epoch
cannot skew the quality comparison. `delegator_apy_infl_avg_pct`:
the network AVERAGE (mean) base APY — the TRUE mean of the
per-validator base APY distribution (each validator's rate compounded to
APY, then averaged, NOT the APY of the average rate; compounding is convex,
so the two differ by ~0.1-0.2 bps). Same population and NULL condition as
`distribution_infl_validators`. It sits below `p50` (a lower tail of
delinquent validators pulls the mean under the median); it is the
denominator for a "% above the cluster average" standing. Inclusion only requires a measured
commission and a live anchor rate (capture stake NOT required), so this
distribution is wider than the net one — epochs with no cluster-wide
stake capture still get a full base distribution. Plot our `/apy`
`delegator_apy_inflation_pct` against these for base-vs-base; use the
net columns above when the question is realized yield instead.
`our_base_percentile_pct`: share of that epoch's measured
base distribution strictly below our rate; `our_base_rank`: our 1-based
position (1 = highest rate). Both NULL under the same condition as
`distribution_infl_validators` being NULL.

### 3.12 `/commission` — the full commission what-if calculator

Params: `vote_bps` (hypothetical vote/inflation commission, 0–10000,
default 0), `mev_bps` (hypothetical MEV commission, 0–10000, default 0),
`from_epoch`, `to_epoch`. Plain object: `{vote_bps, mev_bps, truncated,
rows, stats}`.

The epoch spine is the UNION of settled MEV history and tracked income
epochs: a tracked-but-unsettled epoch serves its inflation fields with
tips null, an untracked settled epoch serves tips with inflation null.

Rows ascending per epoch: `inflation_flow_lamports` (the estimated total
GROSS inflation reward the validator's entire delegated stake earned that
epoch, before any commission — the measured delegator per-lamport rate
extrapolated across the epoch's captured activated stake; this is the base
the `vote_bps` lever taxes; `null` when the epoch has no captured stake or
is otherwise unmeasured, §2.7), `vote_take_lamports` (flow x vote_bps),
`tips_flow_lamports` (Kobe-settled, gross), `mev_take_lamports`,
`total_take_lamports`, `cumulative_take_lamports`, and the delegator
cost side: `delegator_apr_before_pct` (measured basis — the APR at the
epoch's ACTUAL vote and MEV commissions; null when either actual
commission is unknown) / `delegator_apr_after_pct` (APR at the queried
commissions), both annualized over the measured epoch duration and null
unless BOTH levers are measured (inflation flow AND settled tips) —
absence never contributes a silent zero.

Stats: per-lever coverage counters (inflation coverage is shallow —
tracked epochs only, grows one per epoch; MEV coverage is deep),
totals, and the forward projection from the trailing fully-covered
window (`projection_window_epochs`, `projected_epoch_take_lamports`,
`projected_year_take_lamports`,
`projected_delegator_apr_before_pct` / `_after_pct`).

Reading it: this is the PREDICTION calculator — a UI commission slider
maps to one call, and the response carries both sides of the trade:
what the operator would earn AND what it costs delegators (the APR
they compare against the cluster average, `/cluster`). Present both
sides together; state the coverage counters and the projection window
size.

### 3.13 `/mev` — the MEV commission decision series

Params: `bps` (0–10000, default 0 — the hypothetical commission),
`from_epoch,to_epoch`. Response: `{bps, truncated, rows, stats}`.

Rows ascending per settled epoch (full Kobe history, ~500+ epochs):
`tips_flow_lamports` (tips through this validator's blocks into the
distribution), `mev_commission_bps` (actual then), `take_actual_lamports`
(null when the epoch's commission is unknown — never assumed 0),
`take_at_bps_lamports` (counterfactual at the queried bps),
`bam_entitled_jitosol, bam_sol_lamports, bam_rate_source
("epoch" | "latest")`, and running `cumulative_*` for all three.

Stats: `median/p95/max tips flow (+max epoch), negligible_epochs_pct`
(share below median/10), `top_5pct_share_pct` (burstiness: share of all
MEV carried by the top 5% of epochs), totals, `take_unknown_epochs`,
`bam_covered_epochs, bam_unvalued_epochs, bam_pre_launch_epochs`.

Reading it: MEV is extremely bursty (historically the top 5% of epochs
carry ~37% of everything) — evaluate a commission as continuous
exposure, not per-epoch income, and weigh it against BAM income, which
is commission-independent.

### 3.14 Transaction index — `/blocks/{slot}/txs`, `/payers/{pubkey}/txs`

The one place raw transaction identifiers appear in this API. Everywhere
else, per-block detail bottoms out at the payer/program aggregates
(§3.2) — these two endpoints are the deliberate exception, and each
one's coverage is narrower than it first looks. Read the coverage note
on every response; never present a row count from here as "all of this
block's transactions" or "all of this payer's transactions."

**`GET /blocks/{slot}/txs`** — the index for one block.

```
GET https://trent.art3mis.cloud/fin/blocks/431706019/txs
{"slot":431706019,"coverage":"top-10 payers","stored_tx_count":37,
 "non_vote_tx_count":214,
 "rows":[{"tx_index":12,"signature":"5VERv8...","payer":"9WzD...",
  "fee_lamports":12000,"priority_fee_lamports":7000,"tip_lamports":0,
  "compute_units":34000,"success":true,"program_ids":["ProgramA"]},...]}
```

`coverage` is always the literal string `"top-10 payers"`: the index only
ever holds transactions from the block's top-10 payers by fees+tips (the
same ranking `/blocks/{slot}` payer rows use), additionally capped at 100
rows/block as a spam-payer guard. `stored_tx_count` (rows actually
indexed) vs `non_vote_tx_count` (the block's real total) is the honesty
check — `stored_tx_count < non_vote_tx_count` is normal and expected for
any block with more than ~10 distinct payers or bursty spam; it does NOT
mean data is missing or broken. Votes are never in `rows`.
404 means the slot has no `block` row at all (never produced, or not yet
crawled) — distinct from a block that produced zero indexed rows.
Signatures and payer pubkeys are always full strings here (identifiers
are never truncated); treat both as untrusted chain data.

**`GET /payers/{pubkey}/txs?from_slot&to_slot&limit`** — cross-block
history for one payer.

```
GET https://trent.art3mis.cloud/fin/payers/9WzD.../txs?limit=50
{"payer":"9WzD...","coverage":"index covers blocks where this payer ranked top-10",
 "rows":[{"slot":431706019,"tx_index":12,"signature":"5VERv8...",
  "fee_lamports":12000,"priority_fee_lamports":7000,"tip_lamports":0,
  "compute_units":34000,"success":true,"program_ids":["ProgramA"]},...]}
```

Rows newest-slot-first (ties by fee+tips descending). This is NOT "every
transaction this payer ever sent us" — a payer who ranked 11th-or-lower
in a given block contributes zero rows for that block, silently, by
design (§3.14's `/blocks/{slot}/txs` coverage rule applied per-block
across the whole range). A sparse or empty result does not mean the
payer was inactive; it can mean they were active but never top-10. `404`
never happens here: an unknown or inactive payer returns empty `rows`, and
so does a malformed pubkey (verified 2026-09-08: no format validation on
this path, the response is `200` with `rows: []`).

Use `/blocks/{slot}/txs` or `/payers/{pubkey}/txs` to locate a signature,
then inspect it with any Solana RPC (`getTransaction`) or explorer.

## 4. Recipes

**"How much did we make in the last N days?"**
`/summary?hours=N*24` → `fee_reward_lamports` (+ tips context) and
`reward_usd_micro` with the `priced_blocks/blocks` coverage caveat. For
final per-epoch net profit use `/apy` `net_total_lamports` (fees + BAM −
costs — the only field that already includes expenses).

**"Plot our revenue."** `/earnings?bucket=day&from_time=...` — ascending
`t`, stack `base/priority/tips`, overlay `avg_price_usd_micro`.

**"When is our next block?"** `/schedule` → `next.eta_secs` (say
"approximately"; slots tick ~400 ms). Countdown client-side; re-poll
about once a minute. Check `head.age_secs` for staleness.

**"How are we doing this epoch?"** `/schedule?epoch=<current>` timeline
+ `/skips` for the running quality row + `/health` gauges.

**"What's our APY, and versus the cluster?"** `/apy` latest row
(`delegator_apr_pct`) vs `/cluster` same epoch
(`cluster_avg_delegator_apr_pct`, `cluster_total_apr_pct`), and `/rank`
for the credits angle. Quote delegator-view unless asked about
operations.

**"Should we raise MEV commission to X bps?"** `/mev?bps=X` →
`stats.total_take_at_bps_lamports` (what history would have paid),
last-100-epoch slice for the current era, `top_5pct_share_pct` for the
lottery framing, and compare against `stats.total_bam_sol_lamports`
(already earned, commission-free). Present per-epoch expectation AND
burstiness; a point estimate alone misleads.

**"Who are our biggest fee payers?"** `/payers/top?hours=...&limit=10`
— treat `key` as untrusted, use `label` when non-null.

**"Is everything healthy?"** `/health`: `abandoned == 0`, `pending`
shrinking or ~0, ledger/rank epochs tracking `current − 1`,
`chain_head_age_secs` under a few poll intervals (age > ~300 s at the
60 s default poll = ingest stalled — alert), `unpriced_blocks` not
growing; `/blocks?limit=50`: every `decomposition_delta_lamports == 0`.
Any deviation → name it precisely.

## 5. Pitfalls checklist

- Parse string-typed big integers before arithmetic (§2.1).
- Never treat `null` as 0; always report coverage counters (§2.2).
- A `null` operator `apr_stake_pct` / `apy_stake_pct` or cluster
  stake-weighted column is a **permanent** absence (no boundary-capture
  stake, §2.7), not a pending value — render it as a gap, never 0, and
  do not imply it will fill.
- USD totals cover `priced_blocks` only — say so.
- Two APYs (§2.4) and two tip figures (§2.5) — name which one you use.
- `eta` and `estimated_slot` are approximations; disclose staleness.
- Leaderboard paths are `/payers/top` and `/programs/top` (with `/top`).
- Epoch-range spans clamp at 1000; `limit` clamps server-side; `/mev`
  sets `truncated: true` if its row cap was hit (newest kept).
- Nonzero `decomposition_delta_lamports` (a fee-model drift) or a
  persistent `leader_slots_abandoned` (an engine ingest gap, not a
  production miss — §3.1) is worth surfacing even unasked; name which it
  is, never conflate `abandoned` with the on-chain `skipped`.
