# Contract: m2-ledger HTTP API (v1, frozen) Base: `http://m2-ledger:8000` (coolify network alias) / public via Traefik later if needed. Auth: `X-API-Key` header. Two key classes: `service` (CLI/Store/indexer) and `admin` (grants, snapshots). All bodies JSON. Amounts are integer credits. ## Endpoints ### `GET /health` → `200 {"status":"healthy"}` ### `GET /balance/{operator_id}` (service) → `200 {"operator_id","balance","as_of"}` — balance derived from tx log. ### `POST /tx/install` (service) — the FR-005 money move Body: `{"buyer","seller","amount","platform_pct"?,"ref"}` (`ref` = listing_id + install id) Semantics: single SQLite transaction — verify `balance(buyer) ≥ amount`, insert buyer→seller net row + buyer→platform cut row, insert license grant, return grant. → `200 {"tx_ids":[..],"grant":{"grant_id","listing_id","operator_id",...}}` → `402 {"error":"insufficient_funds","balance"}` — nothing written. → `409 {"error":"duplicate_ref"}` — same `ref` already settled (idempotency guard). ### `POST /tx/refund` (service) Body: `{"ref"}` → compensating refund rows for that install ref (edge case + FR-005). → `200 {"tx_ids":[..]}`; `404` unknown ref; `409` already refunded. ### `POST /grant/starter` (admin) Body: `{"operator_id","amount"?}` (default from config, 100 cr UNCONFIRMED) → mint→operator `grant` row. → `200 {"tx_id","balance"}` ### `GET /tx?operator_id=&reason=&since=` (service) → `200 {"transactions":[...]}` append-only log view, paginated. ### `POST /snapshot` (admin, also daily cron) Dumps all balances → commits `audit/YYYY-MM-DD.json` to `m2/market-registry` (Forgejo API, token from env). → `200 {"path","commit"}` ### `GET /license/{operator_id}/{listing_id}` (service) → `200 {"grant":...}` or `404` — the apply path's pre-check. ## Invariants (testable) 1. No endpoint UPDATEs/DELETEs tx rows. 2. `balance == fold(tx log)` for every operator after any call sequence (SC-004). 3. Every grant has a non-null paying tx (SC-003). 4. `/tx/install` is all-or-nothing and idempotent by `ref`. 5. Config (not code): `platform_pct` default 0.10, `starter_grant` default 100.