Skip to content

The query endpoint — checkpoint answers without a node

What ships, and the accepted design

Unless a paragraph below is marked as preprod or main today, it describes the accepted design (D-036 to D-040): active, parked holding the hash, or convicted; no withdraw; the reap by the next keys; deposit is the unfreeze; poison epoch-local and cleared by rotation. Play the checkpoint simulation. What ships today is the V1 checkpoint with role addresses.

ckeri-query is a hosted, read-only HTTP service that answers checkpoint, board, and watchability questions about M1 identities directly from the indexed chain state. A person with only curl — no Cardano node, no local RocksDB store, no Koios key — can ask the public endpoint whether an AID's checkpoint is current and get back an answer that says exactly which indexed chain slot it came from and how stale that slot is.

The production instance is:

https://ckeri.dev.plutimus.com

Routes

Method Path Always 200? Description
GET /ready yes Service and freshness status
GET /checkpoint/{aid} no (503 when not ready) Current authenticated checkpoint for an AID
GET /board no (503 when not ready) Complete authenticated endpoint-board catalog
GET /board/{witness_key} no (503 when not ready) Authenticated endpoint-board record for a witness key
GET /watchability/{aid} no (503 when not ready) Whether a checkpoint's declared witnesses are all listed on the board
GET /swagger.json yes Machine-readable OpenAPI document
GET /swagger-ui yes Interactive Swagger UI over /swagger.json

{aid} is a self-addressing CESR E-code identifier. {witness_key} is a non-transferable Ed25519 CESR B-code key. Both are the qualified-base64 (qb64) form ckeri itself prints — the same strings you already see from ckeri status or a registration transcript.

GET /ready

Always returns HTTP 200 with this exact shape:

{
  "ready": true,
  "as_of_slot": 129921673,
  "tip_lag_slots": 0,
  "upstream": "connected",
  "reason": null
}
$ curl -s https://ckeri.dev.plutimus.com/ready | jq

ready is true only when all of the following hold at once: the store has a transactional watermark, the upstream node connection is up, a chain tip is known, and tip_lag_slots <= 60. reason explains why the service is not ready when it isn't (for example "upstream disconnected" or "tip lag exceeds the 60-slot freshness threshold"); it is null when ready.

GET /checkpoint/{aid}

$ curl -s https://ckeri.dev.plutimus.com/checkpoint/E... | jq
{
  "aid": "E...",
  "as_of_slot": 129921673,
  "tip_lag_slots": 0,
  "checkpoint": {
    "tx_id": "ccf10efe3b90833374cf712fdbe2b246f88aadf34c170c9074d16754cdf5c6f2",
    "output_index": 0,
    "sequence": 1,
    "native_sequence": 1,
    "current_keys": ["D...", "D...", "D...", "D...", "D..."],
    "current_threshold": { "type": "unweighted", "value": 2 },
    "next_key_digests": ["E...", "E...", "E..."],
    "next_threshold": { "type": "unweighted", "value": 2 },
    "witnesses": ["B...", "B...", "B..."],
    "witness_threshold": 2
  }
}

Substitute the AID you want to look up for E.... An unknown but well-formed AID returns HTTP 200 with "checkpoint": null — a null payload is a real answer ("no current checkpoint exists for this identity"), not an error. A malformed CESR identifier (wrong code, wrong length, invalid base64) returns HTTP 400:

{ "error": "malformed_identifier" }

tx_id and owner_key_hash (board only) render as lowercase hex; every other CESR value renders in canonical qualified-base64 form. A threshold preserves its actual weighted or unweighted shape: an unweighted threshold is {"type":"unweighted","value":N}; a weighted threshold is {"type":"weighted","clauses":[[{"numerator":1,"denominator":2}, ...]]}.

GET /board/{witness_key}

$ curl -s https://ckeri.dev.plutimus.com/board/B... | jq
{
  "witness_key": "B...",
  "as_of_slot": 129921673,
  "tip_lag_slots": 0,
  "board": {
    "aid": "E...",
    "scheme": "https",
    "url": "https://witness.example/",
    "tx_id": "5c98bb45cc3e0879a63aa5807dff7f3809ae934ccbcac54f547c189bb4e8701c",
    "output_index": 0,
    "lovelace": 5000000,
    "owner_key_hash": "3d18237dc14be14284d775a5766016c7c4c432dedce287011701c6c7"
  }
}

An unknown witness key returns HTTP 200 with "board": null. The board record is only served after the endpoint independently validates the frozen board marker, datum, KERI event, and signature — a forged or malformed output fails the entire lookup closed rather than returning a partial or untrusted catalog. Koios is not part of this serving path.

GET /board

$ curl -s https://ckeri.dev.plutimus.com/board | jq
{
  "as_of_slot": 129921673,
  "tip_lag_slots": 0,
  "board": [
    {
      "witness_key": "B...",
      "aid": "E...",
      "scheme": "https",
      "url": "https://witness.example/",
      "tx_id": "5c98bb45cc3e0879a63aa5807dff7f3809ae934ccbcac54f547c189bb4e8701c",
      "output_index": 0,
      "lovelace": 5000000,
      "owner_key_hash": "3d18237dc14be14284d775a5766016c7c4c432dedce287011701c6c7"
    }
  ]
}

The array contains every current authenticated record in deterministic output-reference order; an empty catalog is "board": []. The complete catalog and as_of_slot come from one store transaction. As with the single-record route, one forged or malformed output fails the whole response closed with HTTP 500 rather than returning a partial catalog.

GET /watchability/{aid}

$ curl -s https://ckeri.dev.plutimus.com/watchability/E... | jq
{
  "aid": "E...",
  "as_of_slot": 129921673,
  "tip_lag_slots": 0,
  "watchability": {
    "checkpoint_present": true,
    "witnesses_declared": 3,
    "witnesses_listed": 3,
    "missing_witnesses": []
  }
}

This compares the checkpoint's declared witness set against the current authenticated endpoint board and reports how many declared witnesses are actually discoverable. Unlike /checkpoint and /board, watchability is never null: with no current checkpoint it reports "checkpoint_present": false, zero counts, and an empty missing_witnesses list rather than omitting the field.

Freshness and provenance — what as_of_slot actually promises

Every data response carries as_of_slot and tip_lag_slots beside its payload, and /ready carries the same pair at top level. This is not decoration — it is a provenance claim about the chain state behind the answer, and it means something specific:

as_of_slot is the latest rollback-log slot observed inside the same store transaction that read the checkpoint, board, or watchability data. It is deliberately not the follower's internally tracked "last processed slot": that counter advances after each commit and is retained across a disconnect, so it can read as fresher than the store actually is right after a rollback. Sourcing freshness from the transactional watermark instead means a rollback invalidates the freshness claim by the same mechanism that invalidates the data — there is no window where a stale answer carries an optimistic slot number.

tip_lag_slots is computed by sampling the observed chain tip after that transaction commits, purely to measure lag; it never gates which data was read. When the store watermark is momentarily ahead of the sampled tip, or no tip has been observed yet, or the upstream node is disconnected, or the lag exceeds 60 slots, the service reports itself not ready rather than publish a freshness number it cannot stand behind.

Fail-closed semantics

When /ready would report ready: false, every data route (/checkpoint, both /board forms, /watchability) fails closed with HTTP 503 and the same readiness fields, plus an explicit error tag — never a stale or partial payload:

$ curl -si https://ckeri.dev.plutimus.com/checkpoint/E... 
HTTP/2 503
content-type: application/json
{
  "ready": false,
  "as_of_slot": 129921603,
  "tip_lag_slots": null,
  "upstream": "disconnected",
  "reason": "upstream disconnected",
  "error": "service_unavailable"
}

The 503 body never includes checkpoint, board, or watchability — not even as null — so a client cannot mistake a service-unavailable envelope for a data response with an empty payload. An unexpected internal failure (never an expected fail-closed condition) returns HTTP 500 with {"error":"internal_error"} and no other detail.

Configuration

ckeri-query is an opt-env-conf binary: every setting has a command-line flag and a CKERI_* environment variable.

Flag Env Default Notes
--node-socket CKERI_NODE_SOCKET Cardano node socket path
--network-magic CKERI_NETWORK_MAGIC Cardano network magic
--byron-epoch-slots CKERI_BYRON_EPOCH_SLOTS Byron epoch size for the node-to-client codec
--security-param-k CKERI_SECURITY_PARAM_K Rollback-window security parameter
--start-slot / --start-block-hash CKERI_START_SLOT / CKERI_START_BLOCK_HASH origin Follower start point
--store-path CKERI_STORE_PATH RocksDB indexer directory
--manifest-path CKERI_MANIFEST_PATH Deployment manifest JSON path
--board-address CKERI_BOARD_ADDRESS Endpoint-board address; mandatory for ckeri-query
--port CKERI_PORT 8080 HTTP listen port

--board-address/CKERI_BOARD_ADDRESS is optional in the general follower configuration, but ckeri-query refuses to start without it: /board and /watchability have nothing to authenticate against otherwise, and the binary fails fast with a concise message rather than silently serving an endpoint that can never answer those routes.

One process, no derived state

ckeri-query is a single OS process. It opens exactly one RocksDB store through withRocksDBIndexerRunner, hands the resulting handle and transaction runner to both withChainSyncFollower (the chain-sync consumer) and the HTTP application, and links the follower's async before it starts serving — a follower failure takes the whole process down rather than leaving a server answering from a store nothing is updating any more. There is no second follower, no separate query database, and no replication lag between "what was indexed" and "what the endpoint can see".

The HTTP layer itself owns no cache: no memo table, no secondary index, no IORef, MVar, or TVar holding decoded data. Every response is a pure decode of a fresh read inside one kv-transactions transaction — for /checkpoint and /board that is one transaction; for /watchability the same transaction reads both the checkpoint and the board catalog. There is no rollback-invalidation logic to get wrong, because there is no derived state to invalidate: a rollback simply changes what the next transaction reads.

What this endpoint does not do

ckeri status --aid E... --endpoint https://ckeri.dev.plutimus.com now consumes this contract through the production CLI, delivered by #177. It validates the checkpoint and watchability responses, including their echoed identity and shared freshness envelope, and fails closed without falling back to a local store or Koios.