Explorer API Reference
Base URL: http://localhost:8088 (or your deployed explorer backend).
Health
| Route | Description |
|---|---|
GET /health | Liveness — { "status": "ok" } |
GET /ready | Readiness — { "status": "ready" } |
Blocks
List blocks
GET /api/v1/blocks?limit=20&offset=0
Query params:
| Param | Default | Max |
|---|---|---|
limit | 20 | 100 |
offset | 0 | — |
{
"items": [
{
"height": 118234,
"hash": "0xabc123...",
"tx_count": 3,
"producer": "0x9b6e8af0c70...",
"reward_recipient": "0x9b6e8af0c70...",
"block_reward": 0,
"timestamp": "2026-09-03T12:00:00Z"
}
],
"paging": { "limit": 20, "offset": 0, "total": 20, "has_more": true }
}
Get block
GET /api/v1/blocks/:id
:id is block index or hash. Returns the block directly — not wrapped in items:
{
"height": 118234,
"hash": "0xabc123...",
"parent_hash": "0xabc122...",
"tx_count": 3,
"producer": "0x9b6e8af0c70...",
"reward_recipient": "0x9b6e8af0c70...",
"block_reward": 0,
"timestamp": "2026-09-03T12:00:00Z",
"total_fees": 0
}
total_fees is what the block's author earned in fees, read from the block's tx_fee_earned balance effect. Blocks indexed before the explorer recorded fees keep 0 until they are re-indexed.
Transactions
List transactions
GET /api/v1/transactions?limit=20&offset=0&address=0x...&block=...&type=...
| Param | Description |
|---|---|
address | Filter by sender or involved address — case-insensitive, matches either from or to, with or without the 0x prefix |
block | Only the transactions in the block at this height |
type | Exact match against function_call_type, e.g. Transfer, RidePay, Mint |
status | Exact match against the stored status. See the placeholder note below before relying on this: only one value is ever stored today. |
{
"items": [
{
"hash": "0xdef456...",
"block_height": 118234,
"from": "0x1111111111111111111111111111111111111111",
"to": "0x2222222222222222222222222222222222222222",
"amount": 5000000,
"fee": 1000,
"status": "confirmed",
"function_call_type": "RidePay",
"is_ride_related": true,
"timestamp": "2026-09-03T12:00:00Z",
"referrer": null,
"request_referrer_fee": 0,
"offer_referrer_fee": 0
}
],
"paging": { "limit": 20, "offset": 0, "total": 20, "has_more": true }
}
fee is read from the transaction's tx_fee_paid balance effect, because the node's transaction payload carries no fee field. It is 0 when the sender produced the block (no self-fee), for a RideAcceptance (the node folds that fee into the escrow debit), and for transactions indexed before fees were recorded. status is a placeholder — see below.
Get transaction
GET /api/v1/transactions/:hash
Returns the transaction directly — not wrapped in items — with a few fields the list view omits:
{
"hash": "0xdef456...",
"block_height": 118234,
"from": "0x1111111111111111111111111111111111111111",
"to": "0x2222222222222222222222222222222222222222",
"amount": 5000000,
"fee": 1000,
"status": "confirmed",
"function_call_type": "RidePay",
"is_ride_related": true,
"timestamp": "2026-09-03T12:00:00Z",
"nonce": 7,
"tx_index": 0,
"referrer": null,
"request_referrer": null,
"offer_referrer": null,
"request_referrer_fee": 0,
"offer_referrer_fee": 0
}
Accounts
Get account
GET /api/v1/accounts/:address
Returns the account directly:
{
"address": "0x1111111111111111111111111111111111111111",
"balance": 42000000,
"nonce": 7,
"tx_count": 12,
"activity_count": 15,
"is_contract": false
}
Account activity
GET /api/v1/accounts/:address/activity?limit=20&offset=0
Balance effects and activity feed:
{
"items": [
{
"address": "0x1111111111111111111111111111111111111111",
"kind": "ride_pay",
"label": "Ride payment",
"delta": -5000000,
"direction": "debit",
"amount": 5000000,
"tx_hash": "0xdef456...",
"block_height": 118234,
"tx_index": 0,
"function_call_type": "RidePay",
"counterparty": "0x2222222222222222222222222222222222222222",
"timestamp": "2026-09-03T12:00:00Z"
}
],
"paging": { "limit": 20, "offset": 0, "total": 20, "has_more": true }
}
Validators
GET /api/v1/validators?limit=20&offset=0
Takes the same limit/offset as the list endpoints above (default 20, max 100):
{
"items": [
{
"address": "0x9b6e8af0c70...",
"is_active": true,
"blocks_produced": 3941,
"peer_id": ""
}
],
"paging": { "limit": 20, "offset": 0, "total": 3, "has_more": false }
}
peer_id is a placeholder — see below.
Search
GET /api/v1/search?q=<query>
Exact match only — not a substring or fuzzy search. A plain integer is checked against block height. A hex value, with or without 0x and in any case, is checked against transaction hash and block hash; a 40-hex value also returns an account result (summary "Address with no indexed activity" when the indexer has never seen it). Anything else returns no results. The response has no paging — search takes no limit/offset:
{
"items": [
{ "kind": "transaction", "identifier": "0xdef456...", "summary": "Transaction hash match" }
]
}
Network stats
GET /api/v1/stats
Returns the object directly, like the detail endpoints above:
{
"latest_height": 118234,
"tx_per_second": 0.05,
"total_transactions": 48213,
"active_validators": 3,
"avg_block_time_seconds": 60.0
}
avg_block_time_seconds and tx_per_second are measured over the newest 100 blocks: the time between the first and last of them divided by the gaps, and the transactions they carry divided by that time. Both are 0.0 until two blocks are indexed.
Reserve
GET /api/v1/reserve
The reserve position behind CLT, republished from the treasury's last reconciliation run (see Reserves and Reconciliation). It returns the object directly:
{
"configured": true,
"available": true,
"cache_age_seconds": 12,
"last_run": {
"run_at": "2026-10-06T19:55:02Z",
"onchain_supply": 25000000,
"genesis_allocation": 0,
"treasury_minted": 25000000,
"ledger_liability": 25000000,
"custody_reported": 25000000,
"status": "ok"
}
}
Every figure comes from one run, with that run's run_at. Live chain supply is deliberately not mixed in: a mint moves supply at once and the reserve figure only at the next run, so the pair would disagree routinely and ordinary lag would read as a shortfall. The amounts are in CLT base units, and since USDT has 6 decimals and 1 USD = 1,000,000 CLT, one base unit of CLT is one micro-USDT, so ledger_liability and custody_reported compare directly.
| Answer | Meaning |
|---|---|
"configured": false | This deployment has no treasury behind it. Not an error; the explorer page shows no reserve section. |
"available": false with "error" | The treasury could not be read. The last figures are not served instead, because a page that keeps showing ok while nobody can check reports a verification that is not happening. |
"last_run": null | The treasury is reachable and has never reconciled. |
status | ok, mismatch or over_backed_drift, published as is, including a mismatch. |
The answer is cached for 30 seconds (cache_age_seconds says how old it is) and the fetch gives up after 5 seconds. The backend switches this on with APP_TREASURY_PUBLIC_RECONCILIATION_URL.
Fields that are placeholders today
A few fields in the shapes above never change no matter what happened on chain. That isn't a query bug — each one is a literal value written at the point of ingestion, because the data behind it isn't computed anywhere yet:
| Field | Where it appears | Always | Why |
|---|---|---|---|
status | Transaction list and detail; the status filter on the list endpoint | "confirmed" | Hardcoded at ingestion. Every transaction the indexer sees already passed the node's validation before landing in a block, so there's no pending/failed state to report — but it also means the status query parameter can't currently narrow anything, since exactly one value is ever stored. |
peer_id | Validators | "" | The indexer derives the validator set from block producer addresses; the node's block payload identifies a producer by address only, with no libp2p peer id attached. |
None of these are configurable from the API side; they'll start reflecting reality only once the indexer or the node is extended to compute them.
Error responses
{
"code": "not_found",
"message": "..."
}
Codes: not_found, invalid_request, upstream_error, storage_error.
Pagination
List endpoints wrap results in an envelope:
{
"items": [ ],
"paging": {
"limit": 20,
"offset": 0,
"total": 20,
"has_more": true
}
}
total is not a count of matching rows — the backend never runs that query. It is simply offset + items.len(), the position just past the last item on this page. On every page but a short final one, that's the same as offset + limit; it looks like a running total but isn't one, since it has no idea how many rows exist beyond the page just returned. Don't use it to size a pager.
has_more is true whenever the page returned exactly limit items — it doesn't check that a next page actually has anything in it. If the real remaining count happens to equal limit exactly, has_more reports true for one extra request that then comes back empty. Treat it as "keep paging while this is true," not as a promise that more data exists.
Endpoints that don't use this envelope:
- Detail endpoints (
GET /api/v1/blocks/:id,GET /api/v1/transactions/:hash,GET /api/v1/accounts/:address)GET /api/v1/statsandGET /api/v1/reservereturn the object directly — noitems, nopaging. GET /api/v1/searchreturns{ "items": [...] }with nopaging— it takes nolimit/offsetto page through.
Related
- Explorer Overview
- Hub GraphQL — for app development, not exploration