> **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": ""}` 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 `` 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=` 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`.