-
-
Notifications
You must be signed in to change notification settings - Fork 13
Blockfrost API Compatibility — Progress Tracker
This document is the single source of truth for tracking Blockfrost extension implementation progress, known gaps vs Blockfrost, and links to all related PRs. Update this file as modules are merged or gaps are resolved.
| Module | Endpoints | PR | Merged |
|---|---|---|---|
| Epoch |
/epochs/* — latest, by number, params, stakes, blocks (10 endpoints) |
#780 | Feb 2026 |
| Address |
/addresses/* — info, extended, UTXOs, transactions, total (6 endpoints) |
#784 | Feb 2026 |
| Asset |
/assets/* — list, detail, history, txs, addresses, policy (7 endpoints) |
#780 | Feb 2026 |
| Block |
/blocks/* — latest, by hash/slot/number, txs, next/previous, addresses, CBOR (11 endpoints) |
#811 | Mar 2026 |
| Transaction |
/txs/* — detail, UTXOs, CBOR, metadata, metadata/cbor, redeemers, stakes, delegations, withdrawals, mirs, pool_updates, pool_retires, required_signers (13 endpoints) |
#818 | Apr 2026 |
| Module | Endpoints | Issue | PR | Status |
|---|---|---|---|---|
| Account |
/accounts/{stake} + 11 sub-endpoints (rewards, history, delegations, registrations, withdrawals, mirs, addresses, assets, utxos, transactions) |
#835 | #836 | Open — validated on 6 preprod datasets, 141 / 144 checks matched; only accepted AdaPot /rewards timing limitation remains |
| Pools |
/pools/* — list, extended, retired, retiring, detail, history, metadata, relays, delegators, blocks, updates, votes (12 endpoints) |
#846 | #847 | Open — needs review |
| Scripts |
/scripts/* — list, detail, json, cbor, redeemers, datum, datum/cbor (7 endpoints) |
#851 | #852 | Open — ready to merge |
| Governance |
/governance/dreps/* + /governance/proposals/* (17 endpoints) |
#864 | #865 | Open — reviewers assigned |
| Network |
/, /genesis, /network, /network/eras (4 endpoints) |
— | #866 | Open — partially stubbed |
| Metadata |
/metadata/txs/labels, /{label}, /{label}/cbor (3 endpoints) |
— | #872 | Open — ready to merge |
These are persistent limitations across all merged and in-progress modules. They are tracked here for traceability.
| Gap | Affected endpoints | Root cause | Fix path |
|---|---|---|---|
CBOR format mismatch — returns body-only CBOR (a8xx) instead of full tx envelope (84xx = body + witnesses + metadata) |
/blocks/{hash}/txs/cbor, /txs/{hash}/cbor
|
TransactionProcessor stores body-only CBOR |
Requires schema + processor change (#829) |
tx.size smaller than Blockfrost |
/txs/{hash} |
TRANSACTION_CBOR.CBOR_SIZE is body-only, not full tx size |
Fix alongside CBOR format above |
Off-chain pool metadata always null — name, ticker, homepage, description
|
/pools/{id}, /pools/{id}/metadata, /txs/{hash}/pool_updates
|
yaci-store does not crawl off-chain pool metadata URLs | Requires separate metadata crawler |
Token metadata always null — metadata, onchain_metadata
|
/assets/{asset} |
No token registry / CIP-68 metadata ingestion | Requires token metadata store |
POST /tx/submit not supported |
/tx/submit |
Requires direct node submit API integration | Out of scope for indexer |
| Gap | Affected endpoints | Required config | Notes |
|---|---|---|---|
active_stake null |
/epochs/* |
store.adapot.enabled=true |
Graceful degradation when disabled |
Latest /accounts/{stake}/rewards row can lag Blockfrost by 1 row |
/accounts/{stake}/rewards |
store.adapot.enabled=true |
Accepted AdaPot timing limitation on live data. Reproduced on preprod dataset validation for C_many_regs, F_balanced_history, and L_mixed_reward_history during #836
|
live_stake / live_delegators always "0" |
/pools/{id}, /pools/extended
|
Requires real-time ledger state | Not available in yaci-store — tracked in #847 |
calidus_key always null |
/pools/{id} |
CIP-0119 not indexed | Out of scope |
ratified_epoch / expired_epoch null |
/governance/proposals/* |
store.governance-aggr.enabled=true |
Graceful degradation — tracked in #865 |
json_metadata / bytes null on governance anchors |
/governance/dreps/{id}/metadata, /governance/proposals/{id}/metadata
|
External metadata fetching out of scope | On-chain anchor URL/hash returned only |
| Item | Affected endpoints | Notes |
|---|---|---|
Shared account_rewards DB view is still missing |
/accounts/{stake} |
withdrawable_amount currently matches tested preprod datasets, but it is still computed in the reader instead of a reusable DB view. Follow-up issue should add account_rewards for withdrawable_amount and keep same-slot tie-breaking as later work. |
| Gap | Affected endpoints | Notes |
|---|---|---|
epoch.total_fees mismatch |
/epochs/{number} |
DB epoch.total_fees wrong; correct: SUM(block.total_fees) — #781 (open, assigned) |
| Block distribution query slow |
/epochs/{number}/blocks, /epochs/{number}/blocks/{pool_id}
|
Full scan on block table when filtering by epoch + ordering by slot — #791 (open). Fix: idx_block_epoch_slot already shipped in blockfrost-blocks-index-postgresql.sql
|
| Phantom assets cause pagination shift | /assets |
6 assets from failed/invalid transactions indexed by yaci-store but not by Blockfrost — causes row count drift on deep pages — #795 (open, assigned) |
transaction_metadata duplicate rows on restart |
/metadata/txs/labels, /{label}, /{label}/cbor
|
MetadataRollbackProcessor uses deleteBySlotGreaterThan (exclusive) but the node re-delivers the block at the cursor slot on reconnect — inserts exact duplicates. No unique constraint on (tx_hash, label) — #889 (open) |
invalid_before = 0 ambiguity |
/txs/{hash} |
Cannot distinguish absent from explicitly set to slot 0 without schema change |
cip10 always null |
/metadata/txs/labels |
CIP-10 community registry not integrated — #872 |
supply.circulating / supply.locked / stake.live stub |
/network |
Stubs returning "0" — #866
|
| Plutus JSON field ordering cosmetic diff | /scripts/{hash}/json |
constructor before fields vs Blockfrost reverse — accepted deviation |
/addresses/{address}/extended mirrors base response |
/addresses/{address}/extended |
decimals and has_nft_onchain_metadata unavailable without CIP-68 ingestion |
This project does not use Flyway for indexes. Indexes are managed via the admin CLI index applier:
# Apply all standard + extra indexes
yaci-store-admin-cli apply-indexes
# Apply only extra (optional heavy) indexes
yaci-store-admin-cli apply-extra-indexes
# Check which indexes are missing
yaci-store-admin-cli verify-indexesIndex definitions live in components/dbutils/src/main/resources/:
-
index.yml— standard indexes applied byapply-indexes -
extra-index.yml— optional heavy indexes (e.g., GIN onamounts)
| Index name | Table | Notes |
|---|---|---|
idx_address_utxo_owner_stake_addr |
address_utxo |
Covers /accounts/{stake}/addresses, /utxos
|
idx_withdrawal_address, idx_withdrawal_address_slot
|
withdrawal |
Covers /accounts/{stake}/withdrawals
|
idx_instant_reward_address_slot |
instant_reward |
Covers rewards_sum adapot query |
idx_reward_rest_address_slot |
reward_rest |
Covers rewards_sum adapot query |
All scripts use CREATE INDEX CONCURRENTLY IF NOT EXISTS — safe to run on live databases and idempotent.
TODO: Wire all scripts below into
extra-index.ymlso operators can apply them viaapply-extra-indexesinstead of running SQL manually.
Epoch — #780 / perf issue #791
📋 blockfrost-epoch-index-postgresql.sql
-- Target: block WHERE epoch = ? ORDER BY slot ASC/DESC LIMIT ?
-- Used by: GET /epochs/{number}/blocks, GET /epochs/{number}/blocks/{pool_id}
-- Without this: full scan on idx_block_slot with epoch filter — 654ms on preprod
-- NOTE: also created by block module index script (IF NOT EXISTS guard prevents conflict)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_block_epoch_slot
ON block (epoch, slot);Address — #784
Covered by
index.yml(idx_address_utxo_owner_addr). No extra script needed.
Asset — #780
📋 blockfrost-asset-index-postgresql.sql
-- Target: amounts @> '[{"unit":"..."}]'::jsonb
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_address_utxo_amounts
ON address_utxo USING gin (amounts);
-- Target: assets WHERE unit = ? ORDER BY slot, tx_hash LIMIT ?
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_assets_unit_slot
ON assets (unit, slot) INCLUDE (tx_hash, mint_type, quantity);
-- Target: assets WHERE unit = ? AND policy = ?
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_assets_unit_policy
ON assets (unit, policy) INCLUDE (slot, tx_hash, quantity, asset_name, fingerprint);
-- Target: assets WHERE unit = ? (quantity aggregation)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_assets_unit_qty
ON assets (unit) INCLUDE (quantity);
-- Target: assets WHERE policy = ? GROUP BY unit
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_assets_policy
ON assets (policy) INCLUDE (unit, slot, tx_hash, quantity);
-- Target: first minted tx per unit (DISTINCT ON unit ORDER BY slot, tx_hash)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_assets_mint_unit_slot
ON assets (unit, slot) INCLUDE (tx_hash)
WHERE mint_type = 'MINT';
-- Target: candidate scan by mint time (ORDER BY slot, tx_hash)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_assets_mint_slot_tx_hash
ON assets (slot, tx_hash) INCLUDE (unit)
WHERE mint_type = 'MINT';
-- Target: JOIN transaction ON tx_hash ORDER BY tx_index
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_transaction_tx_hash_tx_index
ON transaction (tx_hash, tx_index);Block — #811
📋 blockfrost-blocks-index-postgresql.sql
-- Target: /blocks/{hash}/txs ordered by tx_index, tx_hash
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_transaction_block_tx_index_tx_hash
ON transaction (block, tx_index, tx_hash);
-- Target: /blocks/epoch/{epoch}/slot/{slot}
-- NOTE: also created by epoch module index script (IF NOT EXISTS prevents conflict)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_block_epoch_slot
ON block (epoch, slot);
-- Target: output-side address extraction for /blocks/{hash}/addresses
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_address_utxo_block
ON address_utxo (block) INCLUDE (owner_addr, owner_addr_full, tx_hash, output_index);
-- Target: spent-side address extraction for /blocks/{hash}/addresses
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tx_input_spent_at_block_spent_tx_hash
ON tx_input (spent_at_block, spent_tx_hash) INCLUDE (tx_hash, output_index);Transaction — #818
📋 blockfrost-transaction-index-postgresql.sql
-- NOTE: Already covered — no index needed for these:
-- transaction.tx_hash → PRIMARY KEY
-- withdrawal.tx_hash → idx_withdrawal_tx_hash (index.yml)
-- assets.tx_hash → idx_assets_tx_hash (index.yml)
-- transaction_scripts.tx_hash → idx_txn_scripts_tx_hash (index.yml)
-- Target: transaction_cbor WHERE tx_hash = ?
-- Used by: GET /txs/{hash}/cbor
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_transaction_cbor_tx_hash
ON transaction_cbor (tx_hash);
-- Target: address_utxo WHERE tx_hash = ?
-- Used by: GET /txs/{hash}/utxos (outputs), output_amount aggregation
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_address_utxo_tx_hash
ON address_utxo (tx_hash);
-- Target: tx_input WHERE spent_tx_hash = ?
-- Used by: GET /txs/{hash}/utxos (inputs), input_count subquery
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tx_input_spent_tx_hash
ON tx_input (spent_tx_hash);
-- Target: delegation WHERE tx_hash = ?
-- Used by: GET /txs/{hash}/delegations
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_delegation_tx_hash
ON delegation (tx_hash);
-- Target: stake_registration WHERE tx_hash = ?
-- Used by: GET /txs/{hash}/stakes
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_stake_registration_tx_hash
ON stake_registration (tx_hash);
-- Target: pool_registration WHERE tx_hash = ?
-- Used by: GET /txs/{hash}/pool_updates
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_pool_registration_tx_hash
ON pool_registration (tx_hash);
-- Target: pool_retirement WHERE tx_hash = ?
-- Used by: GET /txs/{hash}/pool_retires
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_pool_retirement_tx_hash
ON pool_retirement (tx_hash);
-- Target: drep_registration WHERE tx_hash = ?
-- Used by: deposit calculation in GET /txs/{hash}
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_drep_registration_tx_hash
ON drep_registration (tx_hash);Account — #836
📋 blockfrost-account-index-postgresql.sql
Account-specific extra indexes documented for this module:
| Index name | Table | Used by |
|---|---|---|
idx_address_stake_address_addr_full |
address |
/accounts/{stake}/addresses when store.utxo.save-address=true
|
idx_address_utxo_owner_stake_addr_owner_addr |
address_utxo |
/accounts/{stake}/addresses fallback path ordered by payment address |
idx_address_utxo_owner_stake_addr_slot_output_index |
address_utxo |
/accounts/{stake}/utxos, output-side account transaction batch scans |
idx_tx_input_tx_hash_output_index |
tx_input |
unspent checks and address_utxo -> tx_input joins |
idx_tx_input_spent_tx_hash |
tx_input |
spending-side transaction lookups in /accounts/{stake}/transactions
|
These are the extra account-module indexes documented in addition to indexes already covered by index.yml or existing primary keys.
-- NOTE: current account reader no longer uses address_tx_amount.
-- It reads from address, address_utxo, tx_input, transaction, reward_rest, instant_reward, and withdrawal.
--
-- Already covered in index.yml / existing PKs:
-- idx_address_utxo_owner_stake_addr
-- idx_withdrawal_address
-- idx_withdrawal_address_slot
-- idx_reward_rest_address_slot
-- idx_instant_reward_address_slot
-- idx_delegation_vote_drep_id
-- transaction.tx_hash (PRIMARY KEY)
-- Target: save-address fast path for /accounts/{stake}/addresses
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_address_stake_address_addr_full
ON address (stake_address, addr_full);
-- Target: /accounts/{stake}/addresses ordered by owner_addr
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_address_utxo_owner_stake_addr_owner_addr
ON address_utxo (owner_stake_addr, owner_addr);
-- Target: /accounts/{stake}/utxos and output-side transaction batch scans
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_address_utxo_owner_stake_addr_slot_output_index
ON address_utxo (owner_stake_addr, slot, output_index);
-- Target: unspent checks and joins from address_utxo -> tx_input
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tx_input_tx_hash_output_index
ON tx_input (tx_hash, output_index);
-- Target: spending-side transaction lookups in /accounts/{stake}/transactions
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tx_input_spent_tx_hash
ON tx_input (spent_tx_hash);Pools — #847
📋 blockfrost-pools-index-postgresql.sql
-- NOTE: Already covered — no index needed for these:
-- pool.pool_id → first column of PK (pool_id, tx_hash, cert_index, slot)
-- voting_procedure.gov_action_tx_hash → idx_voting_procedure_gov_action_tx_hash (index.yml)
-- epoch_stake.(epoch, pool_id) → idx_epoch_stake_epoch_pool_id (adapot migration)
-- Target: pool_registration WHERE pool_id = ? ORDER BY epoch DESC
-- Used by: GET /pools/{id}, /metadata, /relays, /updates
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_pool_registration_pool_id_epoch
ON pool_registration (pool_id, epoch DESC);
-- Target: pool_retirement WHERE pool_id = ?
-- Used by: GET /pools/{id} (retirement epoch), /updates
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_pool_retirement_pool_id
ON pool_retirement (pool_id);
-- Target: pool WHERE status = ? ORDER BY pool_id
-- Used by: GET /pools, /pools/retired, /pools/retiring
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_pool_status
ON pool (status, pool_id);
-- Target: epoch_stake WHERE pool_id = ? (no epoch filter — for MAX(epoch) scan)
-- Used by: GET /pools/{id}/delegators
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_epoch_stake_pool_id
ON epoch_stake (pool_id);
-- Target: block WHERE slot_leader = ?
-- Used by: GET /pools/{id}/blocks (looks up blocks by VRF key)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_block_slot_leader
ON block (slot_leader);
-- Target: voting_procedure WHERE voter_hash = ? AND voter_type = 'STAKING_POOL_KEY_HASH'
-- Used by: GET /pools/{id}/votes
-- NOTE: also created by governance index script (IF NOT EXISTS prevents conflict)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_voting_procedure_voter_hash_voter_type
ON voting_procedure (voter_hash, voter_type);Scripts — #852
📋 blockfrost-scripts-index-postgresql.sql
-- NOTE: Already covered — no index needed for these:
-- script.script_hash → PRIMARY KEY (leading column)
-- datum.datum_hash → PRIMARY KEY (leading column)
-- transaction_scripts.tx_hash → idx_txn_scripts_tx_hash (index.yml)
-- Target: transaction_scripts WHERE script_hash = ? AND purpose IS NOT NULL ORDER BY slot DESC
-- Used by: GET /scripts/{hash}/redeemers (paginated)
-- Partial index keeps it small — excludes rows where purpose IS NULL (non-redeemer script associations)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_transaction_scripts_script_hash_slot
ON transaction_scripts (script_hash, slot DESC)
WHERE purpose IS NOT NULL;Governance — #865
📋 blockfrost-governance-index-postgresql.sql
-- NOTE: Already covered — no index needed for these:
-- drep.drep_hash → first column of PK (drep_hash, tx_hash, cert_index, slot)
-- local_drep_dist.drep_hash → first column of PK (drep_hash, epoch)
-- local_treasury_withdrawal.(gov_action_tx_hash, gov_action_index) → first two columns of PK
-- gov_action_proposal.tx_hash → idx_gov_action_proposal_txhash (index.yml)
-- voting_procedure.(gov_action_tx_hash, ...) → idx_voting_procedure_gov_action_tx_hash_gov_action_index (index.yml)
-- delegation_vote.drep_id → idx_delegation_vote_drep_id (index.yml)
-- Target: drep_registration WHERE drep_hash = ? ORDER BY slot DESC
-- Used by: GET /governance/dreps/{id}/updates, /governance/dreps/{id}/metadata (latest anchor)
-- PK is (tx_hash, cert_index) — drep_hash is not the leading column
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_drep_registration_drep_hash_slot
ON drep_registration (drep_hash, slot DESC);
-- Target: voting_procedure WHERE voter_hash = ? AND voter_type = 'DREP_KEY_HASH'
-- Used by: GET /governance/dreps/{id}/votes
-- NOTE: also created by pools index script (IF NOT EXISTS prevents conflict)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_voting_procedure_voter_hash_voter_type
ON voting_procedure (voter_hash, voter_type);Network — #866
No extra indexes needed. Queries use primary keys on
epoch_paramandblock.
Metadata — #872
Covered by
index.yml(idx_txn_metadata_tx_hash,idx_txn_metadata_label). No extra script needed.
| State | Modules | Approx. Endpoints |
|---|---|---|
Merged in main
|
5 | ~47 |
| In open PRs | 6 | ~54 |
| Total when all merged | 11 | ~101 |
Update this file when PRs are merged or gaps are resolved. When opening a new gap-specific issue, link it back to this document for traceability.