# cardano-api A REST API over [cardano-db-sync](https://github.com/IntersectMBO/cardano-db-sync) and [cardano-node](https://github.com/IntersectMBO/cardano-node), built with FastAPI, asyncpg, and Redis. Read queries — address balances, native tokens, transaction history, assets, stake pools, and blocks — are served directly from the db-sync Postgres database. Live UTxO lookups, current protocol parameters, and transaction submission shell out to `cardano-cli` against a local node socket. Access is gated by API keys, and keys can optionally be issued self-service by proving ownership of a wallet that holds a configurable native token (a CIP-8 signature challenge). ## Features - Address balances, native-token holdings, and transaction history from db-sync - Block, transaction, asset, and stake-pool lookups - Live UTxO and protocol-parameter queries via the node - Transaction submission to the network - Redis response caching with per-endpoint TTLs - Tiered API keys with per-tier rate limits - Optional token-gated, self-service key issuance via CIP-8 wallet signatures - Strict input validation (bech32 addresses, hex tx hashes / policy IDs) and a body-size cap on transaction submission ## Requirements - A synced `cardano-db-sync` Postgres database (the `cexplorer` schema) - A running `cardano-node` with a reachable IPC socket — required only for the node-backed endpoints (live UTxOs, protocol params, tx submit) - Redis — rate limiting, response cache, and API-key storage - `cardano-cli` on `PATH` — the provided Docker image installs it for you ## Quick start The container image bundles `cardano-cli`; you provide the db-sync database, a node socket, and Redis. A minimal `compose.yml`: ```yaml services: cardano-api: build: . ports: - "8765:8765" environment: DB_HOST: postgres-dbsync DB_NAME: cexplorer DB_USER: dbsync DB_PASS: ${DB_PASS} REDIS_HOST: redis API_MASTER_KEY: ${API_MASTER_KEY} CARDANO_NODE_SOCKET_PATH: /node-ipc/node.socket CARDANO_NETWORK: mainnet volumes: - /path/to/node-ipc:/node-ipc redis: image: redis:7 ``` ``` docker compose up -d --build ``` The API listens on port `8765`. Put it behind your own reverse proxy / TLS terminator as needed. To run without Docker: ``` pip install -r requirements.txt uvicorn main:app --host 0.0.0.0 --port 8765 ``` (Install `cardano-cli` separately if you need the node-backed endpoints.) ## Configuration All configuration is via environment variables: | Variable | Default | Description | |---|---|---| | `DB_HOST` | `postgres-dbsync` | db-sync Postgres host | | `DB_PORT` | `5432` | db-sync Postgres port | | `DB_NAME` | `cexplorer` | db-sync database name | | `DB_USER` | `dbsync` | db-sync database user | | `DB_PASS` | _(empty)_ | db-sync database password | | `REDIS_HOST` | `redis-api` | Redis host | | `REDIS_PORT` | `6379` | Redis port | | `API_MASTER_KEY` | _(empty)_ | Unrestricted master key for `/admin/*` | | `CARDANO_NODE_SOCKET_PATH` | `/node-ipc/node.socket` | Node IPC socket path | | `CARDANO_NETWORK` | `mainnet` | Cardano network | `TRUSTED_PROXIES` in `main.py` controls which client IPs are allowed to set the `X-Forwarded-For` header. It defaults to loopback plus the Docker bridge gateways; override it to match your own proxy setup. ## Authentication & tiers Pass an API key in the `X-API-Key` header (preferred) or as an `?api_key=` query parameter. Requests with no key are treated as anonymous. | Tier | Token balance | Rate (req/min) | tx submit (/min) | Node read | Node submit | |---|---|---|---|---|---| | anonymous | 0 | 20 | 0 | no | no | | standard | ≥ 50 | 100 | 2 | yes | no | | elevated | ≥ 500 | 1000 | 10 | yes | yes | | master | n/a | unlimited | unlimited | yes | yes | Anonymous requests are rate-limited per source IP; authenticated tiers are rate-limited per key. The master key is supplied out-of-band via `API_MASTER_KEY` and can mint non-expiring keys at `POST /admin/keys`. ### Token-gated keys (optional, self-service) A wallet that holds enough of a configured native token can mint its own API key by signing a challenge nonce with CIP-8. The gating token's policy ID and the tier thresholds are constants near the top of `main.py` — point them at your own token to use this flow. Token-gated keys expire 48 hours after issue and can be re-authenticated (which also re-tiers them) via `POST /v1/auth/refresh`. A background task re-checks balances every 10 minutes and re-tiers keys in place. ``` POST /v1/auth/challenge { "address": "addr1..." } -> { "nonce": "...", "expires_at": "..." } # sign the nonce with the wallet via CIP-8 POST /v1/auth/verify { "address", "nonce", "signature", "key" } -> { "api_key": "capi_...", "tier", "trp_balance" } POST /v1/auth/refresh (X-API-Key header — self-service only) -> { "tier", "trp_balance", "expires_at", ... } ``` ## Endpoints ``` GET /health GET /v1/sync/status GET /v1/block/latest GET /v1/block/{block_no} GET /v1/address/{address}/balance GET /v1/address/{address}/tokens?page=&limit= GET /v1/address/{address}/transactions?page=&limit=&order= GET /v1/address/{address}/utxos (standard+) GET /v1/tx/{tx_hash} POST /v1/tx/submit (elevated+) GET /v1/asset/{policy_id}/info?page=&limit= GET /v1/asset/{policy_id}/{asset_name}/holders?limit= GET /v1/pool/{pool_id}/info GET /v1/protocol-params (standard+) POST /v1/auth/challenge POST /v1/auth/verify POST /v1/auth/refresh POST /admin/keys (master) DELETE /admin/keys/{key} (master) GET /admin/keys (master) POST /admin/refresh-tiers (master) GET /admin/stats (master) ``` ## Caching Responses are cached in Redis with per-endpoint TTLs (seconds): ``` balance 60 asset_info 120 tokens 60 pool_info 120 transactions 30 sync_status 5 block_latest 10 protocol_params 300 tx_details 300 utxos 10 ``` ## Security notes - API keys are stored as `sha256(key)`. The raw key is returned exactly once, at creation; lookups, listing, and revocation all operate on the hash. - Every path parameter is validated against a strict regex (bech32 mainnet/testnet addresses, 64-hex tx hashes, 56-hex policy IDs) before any query runs. - `POST /v1/tx/submit` bodies are capped at 64 KB. The middleware reads the actual request stream, so chunked transfer or a missing `Content-Length` can't bypass the limit. - CIP-8 verification rejects non-EdDSA algorithms, wrong key length, empty or mismatched payloads, and any key that doesn't hash to the claimed address. ## Contributing Issues and pull requests are welcome. Please keep changes focused, run the app against a local db-sync + node before submitting, and don't commit secrets — a gitleaks workflow scans every push and pull request. ## License Released under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). See [LICENSE](LICENSE).