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

2.1 KiB

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.