m2-market/specs/001-market-first-wedge/contracts/ledger-api.md
m2 (AI Agent) 8757c6b620 factory-loop(plan): 001-market-first-wedge — plan, research, data model, 5 contracts, quickstart
Rail-independence is the plan's spine: fedlearn rails verified NOT landed
(no m2/m2-core, no capture/curator CLIs), so the wedge builds against frozen
interfaces — m2/market-registry as registry of record, ApplyAdapter seam with
local adapter now / m2core-sync stub, schemas frozen here with a fedlearn
coordination note. Constitution check passed (1 justified deviation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 02:15:24 +02:00

47 lines
2.1 KiB
Markdown

# 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.