Indexer
ImplementedHistorical reads come from an indexer. Live state never does.
#The split
| Data | Source | If it is down |
|---|---|---|
| Vault balances, snapshots, pause flags, NAV | Chain, every request | Nothing to be down |
| Positions and divergence | Chain, every request | Nothing to be down |
| Vault activity and 24-hour volume | Chain, into an in-memory log | Falls back to the last good read |
| Receipt history | Indexer, falling back to the chain log | Window shortens to 24 hours, and the response says so |
| Returns (24H / 7D / 30D) | Indexer NAV series | No return is shown, and it says why |
The database is a read model. It is never authoritative: no vault decision, no share price, and no eligibility result is ever taken from it.
#What serves history
Robinhood Chain produces a block roughly every 100 ms, so scanning from genesis on demand is not viable. Two things answer instead, and a caller is always told which one did.
The chain log. The API keeps a rolling 24-hour window in memory, back-filled once at startup in chunks small enough for a provider to serve, then tailed as new blocks arrive. It needs no infrastructure and cannot be stale. It also cannot answer for anything older, because nothing is retained that long.
The window is measured in seconds, not blocks. A block-counted window is only equivalent to "24 hours" while blocks arrive on schedule, and the count it produces feeds source eligibility — so a window that quietly stretched would also make accounts look eligible when they are not.
The indexer. Where it is running, /v1/receipts is served from PostgreSQL
and reaches back as far as ingestion has got. The API prefers it and falls back
to the chain log when it cannot be read.
#Why every response says where it came from
The fallback has a failure mode worth naming: when the database goes away, the answer becomes shorter, not absent. A caller asking for six months would get 24 hours, with a 200 and no error — and a short list is indistinguishable from a complete one. "The database is down" would read as "nothing happened".
So every receipts response carries coverage:
{
"source": "indexer",
"windowSeconds": null,
"oldestBlock": "44",
"degraded": false,
"reason": null
}windowSeconds is null for the indexer, which is bounded by how far it has
ingested rather than by a configured window — oldestBlock is the real answer
to "how far back". When the chain log answers instead, windowSeconds is 86400.
degraded is the load-bearing field. It is true only when the indexer should
have answered and could not, so a stack deliberately running without a database
is never flagged, while one whose database has failed always is. The interface
shows that state rather than quietly rendering a shorter list.
Running without DATABASE_URL is a supported configuration. It costs history
beyond a day, and nothing else.
#Reorganisation handling
Orbit chains give fast soft confirmation from the sequencer, but an L1 reorganisation can still reorganise unfinalised L2 blocks.
INDEXER_CONFIRMATIONS sets the depth treated as final. Rows shallower than that are ingested and marked soft, and every surface that displays them labels them unfinalised.
#Configuration
| Setting | Purpose |
|---|---|
INDEXER_START_BLOCK | Where ingestion begins |
INDEXER_CONFIRMATIONS | Depth treated as final |
INDEXER_BATCH_SIZE | Logs per range request |
DATABASE_URL | PostgreSQL connection |
Note. With no
DATABASE_URL, the API starts anyway and reports the database as not configured in/v1/status. Live vault state and 24-hour activity are unaffected, because they are read from chain. Receipt history is served from the chain log instead, and itscoveragesays so. A missing read model degrades the product; it does not break it.
#The NAV series
A return needs two points in time, and the chain only ever shows one. The
indexer records what one share is worth on a cadence
(INDEXER_NAV_INTERVAL_SECONDS), using the vault's own
convertToAssets(1e18) — the contract's answer to what a redeemer would get,
virtual offset included, rather than a ratio computed here that could disagree
with it.
Per share, not per vault. A vault that doubles its TVL through deposits has returned nothing to anyone. A series built on total value would report a 100% gain for taking someone's money, which is the single most misleading number this product could publish.
Gaps are recorded as gaps. When an asset has no trustworthy price the vault refuses to report NAV, and the snapshot is written with a null value and the contract's reason rather than a zero. Returns skip those points instead of measuring from them: a return computed from a fabricated zero would show a total loss followed by a total recovery, neither of which happened.