# S1 — m2-ledger SPEC: Ledger & Economy ## 1. Scope & Non-Goals ### MVP scope `m2-ledger` is a standalone Coolify app on the shared `coolify` Docker network with Docker DNS alias `m2-ledger` and private base URL `http://m2-ledger:8000`. It is the append-only credit ledger for the M2 Marketplace wedge: - Whole-number M2 credits (`m2cr`) only. - SQLite as the authoritative store for MVP. - Append-only transaction log; balances are derived from transactions. - Account creation and starter grants for operators, sellers, platform, resource owners, and system accounts. - Fixed-price install settlement: buyer debit, seller credit net of platform cut, platform cut, license grant, idempotent install ref. - Job-based pricing settlement for service-style Solutions: escrow, milestone release, acceptance, dispute/manual cancel, and operator earnings through the same ledger. - Route/resource settlement hooks so m2-gpt token/resource spend can be represented as ledger `route`/`job` events without duplicating m2-gpt metering. - Federation with m2-gpt identity: every service call made with a gateway-issued tenant/agent key or service key resolves to a server-side `(tenant_id, agent_id, operator_id)` before ledger mutation. - Daily snapshot-to-Forgejo audit: balance and transaction-hash snapshots committed to Forgejo org `m2`. - Admin-only starter grant default: `100` credits. - Platform cut default: `10%`. ### Later scope - Public fiat purchase and cash payout automation. - On-chain/crypto settlement, public token, custody, exchange, tax, and fraud workflows. - Multi-currency balances. The schema reserves `currency`, but MVP accepts only `m2cr`. - Negative balances, credit lines, chargebacks beyond append-only compensating entries. - Trustless escrow. MVP escrow is an internal account state machine. - Replacing m2-gpt billing. MVP federates with it; later it may move under an extended m2-gpt billing plane. ### Non-goals - Catalog truth, listing review, and install package format. S2/S5 own those. S1 consumes `listing_id`, `solution_id`, version, price, seller, and job terms. - Storefront UX. S3 owns cargstore/M2 Store. - Scout/Hermes proposal ranking. S4/S6 own discovery/proposals; S1 supplies quote, authorization, and settlement APIs. - Secrets management implementation. S1 requires runtime-injected keys and documents env names only. ## 2. User Stories + Acceptance Criteria ### Install purchase Given an operator has a positive M2 credit balance and a published fixed-price listing, when M2 Store or `m2-market install` calls `POST /v1/settlements/install`, then the ledger atomically records buyer debit, seller net credit, platform cut, and a license grant. Acceptance: - Balance is sufficient before any rows are inserted. - Repeated calls with the same `idempotency_key` return the existing settlement. - No grant exists without a paid transaction group. - Platform cut defaults to 10% unless listing revenue split overrides it. ### Insufficient funds Given a buyer balance is below the required price, when install settlement is requested, then the API returns `402 insufficient_funds` and writes no transactions, grants, or ledger events. ### Starter grant Given an active operator account and an admin/service key with `ledger:grant`, when `POST /v1/grants/starter` is called without amount, then the account receives exactly 100 credits from `acct_mint`. ### Job-based Solution Given a buyer accepts an operator-delivered job quote, when `POST /v1/jobs` is called, then funds move from buyer to `acct_escrow`, a job record is opened, and the assigned operator can claim milestones only through explicit acceptance or admin/manual release rules. Acceptance: - `quoted -> funded -> in_progress -> submitted -> accepted -> paid` is the happy path. - Milestone payments release from escrow to seller and platform via append-only rows. - Cancel before work returns escrow to buyer through compensating rows. - Dispute freezes remaining escrow until an admin resolution event. ### Route/resource settlement Given m2-gpt records metered model spend in its `spend_log` and rollups, when an authorized route-settlement worker posts a route usage batch, then m2-ledger records a credit-denominated `route` transaction group tied to the m2-gpt source IDs, without modifying m2-gpt records. ### Audit snapshot Given the daily cron runs, when `/v1/admin/snapshots` executes, then it writes a deterministic JSON snapshot containing balances, tx high-water mark, previous snapshot hash, and current tx chain hash, and commits it to Forgejo. ## 3. Interfaces & Data Contracts ### Runtime deployment contract - Coolify app: `m2-ledger`. - Network: `coolify`. - Internal URL: `http://m2-ledger:8000`. - Public route: none for MVP unless an admin UI needs it; prefer internal network calls. - Health: `GET /health -> {"status":"healthy","service":"m2-ledger","db":"ok"}`. - Required env, injected at runtime: - `M2_LEDGER_DB=/data/m2-ledger.sqlite3` - `M2_LEDGER_API_KEYS_FILE=/run/secrets/m2-ledger-keys.json` - `M2_GPT_BASE_URL=https://gpt.machinemachine.ai` - `M2_GPT_ADMIN_TOKEN` or m2-gpt service key for identity introspection - `FORGEJO_URL=https://git.machinemachine.ai` - `FORGEJO_ORG=m2` - `FORGEJO_AUDIT_REPO=market-audit` - `FORGEJO_TOKEN` - `PLATFORM_CUT_BPS=1000` - `STARTER_GRANT_CREDITS=100` No secrets are committed to repos or images. ### Auth and identity MVP supports two accepted caller modes: 1. `Authorization: Bearer sk-m2-...` for gateway-issued tenant/agent keys. m2-ledger introspects through m2-gpt or validates a signed identity assertion issued by m2-gpt. 2. `X-API-Key: ...` for service/admin keys stored only in runtime secrets. Resolved identity object: ```json { "tenant_id": "m2", "agent_id": "chris-m2o", "operator_id": "op_chris", "principal_ref": "agent:chris-m2o", "scopes": ["ledger:read", "ledger:install"], "auth_source": "m2-gpt" } ``` Rules: - Request bodies may include `operator_id` only for admin/service operations. Buyer identity for agent/user calls is server-derived from auth. - Cross-tenant reads return `404`, not existence-revealing `403`, unless caller has `ledger:admin`. - Service keys are scoped: `ledger:install`, `ledger:job`, `ledger:route`, `ledger:grant`, `ledger:snapshot`, `ledger:admin`. - All mutations require `Idempotency-Key` header or `idempotency_key` body field; both present must match. ### SQLite tables #### `accounts` ```sql CREATE TABLE accounts ( account_id TEXT PRIMARY KEY, account_type TEXT NOT NULL CHECK (account_type IN ('operator','tenant','platform','mint','escrow','resource','service')), operator_id TEXT, tenant_id TEXT, display_name TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('active','suspended','closed')), metadata_json TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL ); ``` System accounts: - `acct_mint`: source for starter grants and admin corrections. - `acct_platform`: platform cut receiver. - `acct_escrow`: escrow holder for funded jobs. #### `ledger_tx` ```sql CREATE TABLE ledger_tx ( tx_id TEXT PRIMARY KEY, tx_group_id TEXT NOT NULL, seq INTEGER NOT NULL, ts TEXT NOT NULL, from_account_id TEXT NOT NULL REFERENCES accounts(account_id), to_account_id TEXT NOT NULL REFERENCES accounts(account_id), amount INTEGER NOT NULL CHECK (amount > 0), currency TEXT NOT NULL CHECK (currency = 'm2cr'), tx_type TEXT NOT NULL CHECK (tx_type IN ('install','payout','grant','route','earn','job','refund','adjustment')), ref_type TEXT NOT NULL, ref_id TEXT NOT NULL, idempotency_key TEXT NOT NULL, actor_tenant_id TEXT, actor_agent_id TEXT, actor_operator_id TEXT, metadata_json TEXT NOT NULL DEFAULT '{}', prev_hash TEXT NOT NULL, tx_hash TEXT NOT NULL, UNIQUE(tx_group_id, seq), UNIQUE(idempotency_key, tx_type) ); ``` Append-only invariant: no `UPDATE` or `DELETE` application paths. Corrections use new rows with `refund` or `adjustment`. Balance formula: ```sql SUM(CASE WHEN to_account_id = :account THEN amount ELSE 0 END) - SUM(CASE WHEN from_account_id = :account THEN amount ELSE 0 END) ``` Non-system accounts may not go negative. `acct_mint` can issue grants. `acct_escrow` may only release against funded job groups. #### `license_grants` ```sql CREATE TABLE license_grants ( grant_id TEXT PRIMARY KEY, operator_id TEXT NOT NULL, account_id TEXT NOT NULL REFERENCES accounts(account_id), tenant_id TEXT NOT NULL, listing_id TEXT NOT NULL, solution_id TEXT NOT NULL, solution_version TEXT NOT NULL, major_version INTEGER NOT NULL, tx_group_id TEXT NOT NULL, install_ref TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('active','revoked','refunded')), created_at TEXT NOT NULL, UNIQUE(operator_id, listing_id, major_version) ); ``` #### `jobs` ```sql CREATE TABLE jobs ( job_id TEXT PRIMARY KEY, listing_id TEXT, solution_id TEXT, buyer_account_id TEXT NOT NULL REFERENCES accounts(account_id), seller_account_id TEXT NOT NULL REFERENCES accounts(account_id), tenant_id TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('quoted','funded','in_progress','submitted','accepted','paid','cancelled','disputed')), total_amount INTEGER NOT NULL CHECK (total_amount > 0), currency TEXT NOT NULL CHECK (currency = 'm2cr'), platform_cut_bps INTEGER NOT NULL DEFAULT 1000, terms_json TEXT NOT NULL, escrow_tx_group_id TEXT, accepted_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); ``` #### `job_milestones` ```sql CREATE TABLE job_milestones ( milestone_id TEXT PRIMARY KEY, job_id TEXT NOT NULL REFERENCES jobs(job_id), idx INTEGER NOT NULL, title TEXT NOT NULL, amount INTEGER NOT NULL CHECK (amount > 0), acceptance_mode TEXT NOT NULL CHECK (acceptance_mode IN ('buyer_accept','auto_after_timeout','admin_release')), due_at TEXT, status TEXT NOT NULL CHECK (status IN ('pending','funded','submitted','accepted','released','cancelled','disputed')), release_tx_group_id TEXT, evidence_uri TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(job_id, idx) ); ``` #### `snapshots` ```sql CREATE TABLE snapshots ( snapshot_id TEXT PRIMARY KEY, snapshot_date TEXT NOT NULL UNIQUE, tx_high_water_seq INTEGER NOT NULL, tx_chain_hash TEXT NOT NULL, prev_snapshot_hash TEXT, snapshot_hash TEXT NOT NULL, forgejo_commit TEXT, forgejo_path TEXT NOT NULL, created_at TEXT NOT NULL ); ``` ### HTTP API All endpoints are under `/v1` except health. #### Accounts and balances `GET /v1/accounts/me` Response: ```json { "operator_id": "op_chris", "tenant_id": "m2", "account_id": "acct_op_chris", "status": "active" } ``` `GET /v1/balances/{account_or_operator_id}` Requires same operator/tenant or `ledger:admin`. Response: ```json { "account_id": "acct_op_chris", "operator_id": "op_chris", "balance": 140, "currency": "m2cr", "as_of_tx_seq": 1234, "as_of": "2026-07-02T00:00:00Z" } ``` `GET /v1/transactions?account_id=&operator_id=&tx_type=&ref_type=&ref_id=&since=&limit=&cursor=` Returns paginated append-only entries visible to caller. #### Starter grants `POST /v1/grants/starter` Requires `ledger:grant`. Request: ```json { "operator_id": "op_gunnar", "tenant_id": "m2", "amount": 100, "reason": "starter_grant_2026w27", "idempotency_key": "starter:op_gunnar:2026w27" } ``` Response: ```json { "tx_group_id": "txg_01j...", "tx_ids": ["tx_01j..."], "balance": 100 } ``` #### Fixed install settlement `POST /v1/settlements/install` Requires `ledger:install`. Request: ```json { "buyer_operator_id": "op_chris", "seller_operator_id": "op_sdjs", "tenant_id": "m2", "listing_id": "lst_competitor-scan", "solution_id": "sol_competitor-scan", "solution_version": "1.0.0", "install_ref": "lst_competitor-scan-v1.0.0", "amount": 40, "currency": "m2cr", "platform_cut_bps": 1000, "idempotency_key": "install:op_chris:lst_competitor-scan:1.0.0" } ``` Settlement math: - `platform_cut = floor(amount * platform_cut_bps / 10000)`, minimum `1` if `platform_cut_bps > 0` and `amount > 1`. - `seller_net = amount - platform_cut`. - Rows: - buyer -> seller, `seller_net`, `install` - buyer -> platform, `platform_cut`, `install` - License grant created in the same SQLite transaction. Response: ```json { "status": "settled", "tx_group_id": "txg_01j...", "tx_ids": ["tx_01j_a", "tx_01j_b"], "grant": { "grant_id": "gr_01j...", "operator_id": "op_chris", "listing_id": "lst_competitor-scan", "solution_id": "sol_competitor-scan", "solution_version": "1.0.0", "major_version": 1, "status": "active" } } ``` Errors: - `402 insufficient_funds` - `409 duplicate_idempotency_key` - `409 license_already_granted` - `422 invalid_amount_or_split` `POST /v1/settlements/install/{tx_group_id}/refund` Requires `ledger:admin` or service compensation scope. Creates compensating `refund` rows and marks grant `refunded`; does not delete original install rows. #### License check `GET /v1/licenses/{operator_id}/{listing_id}?major_version=1` Used by `m2-core-sync`/install adapter before apply and by M2 Store for installed state. Response: ```json { "grant": { "grant_id": "gr_01j...", "status": "active", "solution_version": "1.0.0", "major_version": 1 } } ``` #### Job quotes and settlement `POST /v1/jobs` Requires `ledger:job`. Request: ```json { "job_id": "job_01j...", "buyer_operator_id": "op_chris", "seller_operator_id": "op_gunnar", "tenant_id": "m2", "listing_id": "lst_agent-scaffold-assisted", "solution_id": "sol_agent-scaffold", "total_amount": 120, "currency": "m2cr", "platform_cut_bps": 1000, "terms": { "summary": "Create and install tailored workspace scaffold", "acceptance": "buyer_accept", "auto_accept_after_hours": 72 }, "milestones": [ {"title": "Scaffold delivered", "amount": 80, "acceptance_mode": "buyer_accept"}, {"title": "Install verified", "amount": 40, "acceptance_mode": "buyer_accept"} ], "idempotency_key": "job:create:job_01j..." } ``` Behavior: - Verifies buyer balance >= total. - Inserts buyer -> `acct_escrow` row with `tx_type=job`, `ref_type=job`. - Creates `jobs.status=funded`. - Creates funded milestone rows. `POST /v1/jobs/{job_id}/start` Seller/operator marks work `in_progress`. No funds move. `POST /v1/jobs/{job_id}/milestones/{milestone_id}/submit` Seller submits evidence. Request: ```json {"evidence_uri":"forgejo://m2/jobs/job_01j/evidence/m1.md"} ``` `POST /v1/jobs/{job_id}/milestones/{milestone_id}/accept` Buyer or admin accepts. Releases milestone amount: - escrow -> seller, `amount - platform_cut` - escrow -> platform, `platform_cut` When all milestones released, job becomes `paid`. If the job produced an installable Solution, the catalog/install subsystem may call fixed install grant separately or include a zero-price license grant only if job terms say delivery includes install rights. `POST /v1/jobs/{job_id}/cancel` Allowed before any milestone release by buyer/admin, or by admin after dispute. Refunds unreleased escrow to buyer and marks pending milestones cancelled. `POST /v1/jobs/{job_id}/dispute` Sets `status=disputed`; no movement until admin release/refund. #### Route/resource settlement `POST /v1/settlements/route` Requires `ledger:route`; intended caller is m2-gpt route-settlement worker or marketplace resource router. Request: ```json { "tenant_id": "m2", "buyer_operator_id": "op_chris", "resource_account_id": "acct_resource_spark", "amount": 3, "currency": "m2cr", "source": { "system": "m2-gpt", "spend_log_ids": ["7fd1..."], "route_id": "spark-glm", "usd": "0.018400", "conversion_rate": {"m2cr_per_usd": 100} }, "idempotency_key": "route:m2:op_chris:2026-07-02T00:15" } ``` Rows: - buyer -> resource owner/platform, `route`. m2-gpt remains the meter of record for tokens/USD; m2-ledger records credit settlement and source IDs only. #### Snapshots and audit `POST /v1/admin/snapshots` Requires `ledger:snapshot`. Also run daily by cron around 00:10 UTC. Response: ```json { "snapshot_id": "snap_2026-07-02", "path": "audit/ledger/2026/07/2026-07-02.json", "commit": "abc123...", "snapshot_hash": "sha256:..." } ``` Snapshot JSON: ```json { "schema_version": "m2.ledger.snapshot.v1", "date": "2026-07-02", "generated_at": "2026-07-02T00:10:00Z", "tx_high_water_seq": 1234, "tx_chain_hash": "sha256:...", "prev_snapshot_hash": "sha256:...", "balances": [ {"account_id":"acct_op_chris","operator_id":"op_chris","tenant_id":"m2","balance":140,"currency":"m2cr"} ] } ``` Forgejo target: `https://git.machinemachine.ai/m2/market-audit`, path `audit/ledger/YYYY/MM/YYYY-MM-DD.json`. If S2 chooses a single `m2/market-registry` repo, use `m2/market-registry/audit/ledger/...`; S1 requires only an append-only Forgejo-backed audit path. ### Events m2-ledger emits JSONL events to stdout for Coolify log collection and optionally `POST /memory/store` for pricing evidence summaries under `agent_id=market:ledger`. Event envelope: ```json { "schema_version": "m2.ledger.event.v1", "event_id": "evt_01j...", "ts": "2026-07-02T00:00:00Z", "type": "install.settled", "tenant_id": "m2", "operator_id": "op_chris", "ref": {"listing_id":"lst_competitor-scan","tx_group_id":"txg_01j..."}, "amount": 40, "currency": "m2cr" } ``` Event types: - `account.created` - `grant.starter_issued` - `install.settled` - `install.refunded` - `license.granted` - `job.funded` - `job.started` - `job.milestone_submitted` - `job.milestone_released` - `job.cancelled` - `job.disputed` - `route.settled` - `snapshot.committed` Memory store payload for non-sensitive pricing evidence: ```json { "agent_id": "market:ledger", "tenant_id": "m2", "memory_type": "episodic", "source": "api", "content": "Install settled: lst_competitor-scan v1.0.0 for 40 m2cr; seller net 36; platform cut 4.", "metadata": { "event_type": "install.settled", "listing_id": "lst_competitor-scan", "solution_id": "sol_competitor-scan", "amount": 40, "currency": "m2cr", "tx_group_id": "txg_01j..." } } ``` No client data, raw job artifacts, secrets, or private tenant-derived content goes into shared memory events. ### CLI verbs `m2-ledger` admin CLI: - `m2-ledger serve` - `m2-ledger migrate` - `m2-ledger account create --operator-id op_chris --tenant-id m2` - `m2-ledger grant starter --operator-id op_chris` - `m2-ledger balance op_chris` - `m2-ledger tx list --operator-id op_chris --limit 20` - `m2-ledger snapshot run` - `m2-ledger audit verify-chain` `m2-market` integration verbs call the HTTP API, not SQLite: - `m2-market wallet balance` - `m2-market wallet tx` - `m2-market install `: quote/listing lookup -> ledger install settlement -> license grant -> runtime-sync apply. - `m2-market job create|start|submit|accept|cancel|dispute` ### File paths - Service DB volume: `/data/m2-ledger.sqlite3`. - Service config: `/etc/m2-ledger/config.toml` generated by Coolify env or mounted secret. - Service keys: `/run/secrets/m2-ledger-keys.json`. - Snapshot staging: `/data/snapshots/YYYY-MM-DD.json`. - Local desktop install state remains owned by S5 path: `~/.m2-market/state.json` and `~/.m2-core/state.json`; ledger only supplies grants and settlement refs. ## 4. Integration Contract With Other Subsystems ### m2-gpt Touchpoints verified in live repo: - Agent keys are minted per tenant/agent and resolve to `(tenant_id, agent_id)`. - Existing tables include `tenants`, `agents`, `api_keys`, `tenant_budgets`, `spend_log`, and daily/weekly rollups. - Harnesses call `https://gpt.machinemachine.ai/v1` using `Authorization: Bearer sk-m2-...`. Contract: - m2-ledger accepts m2-gpt-issued bearer identity for operator-facing reads and purchases. - m2-ledger service/admin operations use runtime-injected service keys. - Route settlement references m2-gpt `spend_log.id`, `tenant_id`, `agent_id`, `route_id`; m2-ledger never edits m2-gpt metering rows. - Future migration path: move `accounts`, `ledger_tx`, `jobs`, and `license_grants` into an extended m2-gpt billing schema, preserving tx hashes and IDs. m2-ledger becomes a compatibility API or billing module. The append-only `ledger_tx` maps to a future `billing_ledger_entries`; `spend_log` maps to source metering; `tenant_rollups_daily.extras` can include credit summary. ### S2 Catalog / registry - S2 provides published listing data: `listing_id`, `solution_id`, `solution_version`, `inventory_type`, `seller`, `price`, `tenant_visibility`, `install_ref`, `revenue_split`. - S1 validates only the settlement payload and tenant visibility reference supplied by trusted Store/CLI. S1 does not own listing truth. - Ledger events update install counts and pricing evidence through S2 indexer, not direct catalog mutation. ### S3 Storefront - M2 Store calls: - `GET /v1/accounts/me` and `GET /v1/balances/{account_or_operator_id}` before purchase display. - `POST /v1/settlements/install` on install click. - `GET /v1/licenses/{operator_id}/{listing_id}` to show installed/licensed state. - Job endpoints for service-style checkout. - Store must present price, seller, platform cut, and permission diff before settlement. ### S4 Scout - Scout never settles directly from a passive proposal. - Scout deep-links Store with `listing_id` and optional `proposal_id`. - If install accepted, Store/CLI includes `proposal_id` in install settlement metadata so ledger events can feed conversion evidence. ### S5 Packaging / runtime-sync - Install order: 1. Resolve listing and permissions. 2. Settle ledger install. 3. Receive license grant. 4. Apply via runtime-sync / `m2-core-sync`. 5. On apply failure, call refund/compensation endpoint if no durable install state was written. - Apply path must check grant via `GET /v1/licenses/...` and write `grant_id` into local install state. - Mixed fleet constraint: no image-specific ledger client dependency; all calls are HTTP and runtime-sync universal. ### S6 Proposal engine / Hermes - Hermes proposal uses ledger quote/balance endpoints to compare build-vs-install and job options. - Agents may spend only within operator-set budgets. For MVP, budget enforcement is split: - m2-gpt enforces token/USD route budgets. - m2-ledger enforces credit balances and job/install affordability. - Proposal records include estimated credit cost but no ledger mutation until explicit approval. ### Forgejo - Forgejo is audit system of record for daily snapshots. - Commit author should be a service identity such as `m2-ledger-bot`. - Snapshots are append-only by path. Corrections use a new snapshot or correction file; never rewrite historical snapshots after publication. ### memory-api - Optional pricing evidence and event summaries are stored under `agent_id=market:ledger`. - `tenant_id` must be explicit; shared market evidence uses `tenant_id=m2`. - Tenant-private job details must not be written to shared memory. Use tenant-scoped memory only when owner-initiated and scrubbed. ## 5. Options Compared ### Ledger substrate | Option | Pros | Cons | Verdict | |---|---|---|---| | Standalone `m2-ledger` SQLite service | Small blast radius, fast MVP, append-only model is easy to audit, deploys as Coolify app | Another service to operate; later migration needed | Recommended for MVP; locked decision | | Extend m2-gpt billing now | Reuses tenant keys, budgets, spend admin UI | Higher regression risk in live gateway; mixes marketplace credit semantics with token metering too early | Later migration target | | Forgejo-as-ledger only | Very auditable | Poor concurrency/idempotency; awkward balance queries; harder escrow | Reject | ### Job settlement model | Option | Pros | Cons | Verdict | |---|---|---|---| | Full escrow at job acceptance | Protects seller capacity, simple affordability check, clear buyer commitment | Buyer locks full funds | Recommended | | Milestone pay-as-you-go without escrow | Lower upfront buyer lock | Seller non-payment risk; harder abuse handling | Later for trusted parties | | Manual invoice only | Simple | Does not close marketplace loop | Reject for MVP | ### Crypto-capable evolution Recommended path: 1. MVP internal credits in `m2-ledger`. 2. Add exportable hash chain and deterministic snapshots from day one. 3. When m2-gpt billing grows a first-class billing schema, migrate ledger tables under m2-gpt while preserving IDs and hashes. 4. Only after legal/tax/custody decisions, add external payment rails or on-chain mirrors as settlement adapters. The internal ledger remains source of entitlement truth until explicitly replaced. ## 6. Risks / Edge Cases - Tenant leakage: job descriptions and evidence can contain client data. Ledger stores refs and scrubbed summaries only; raw artifacts stay tenant-owned. - Cross-tenant account probing: return `404` for inaccessible account/license/job IDs. - Secrets in audit: snapshots include account IDs, balances, tx refs, hashes, not API keys or raw metadata. - Duplicate purchase clicks: idempotency key required; duplicate install ref returns existing grant. - Apply failure after settlement: use compensating refund rows; never delete original install rows. - Platform cut rounding: define basis points and deterministic integer rounding in the API contract. - Seller buys own listing: allow only with explicit `allow_self_purchase=true` admin/test metadata; default reject to prevent fake volume. - Suspended account: cannot send or receive except admin refund/adjustment. - Job abandonment: terms must define auto-accept timeout or admin resolution path; otherwise jobs can lock escrow indefinitely. - Snapshot commit failure: snapshot remains local with `forgejo_commit=null`, service retries; API health reports degraded audit until committed. - SQLite concurrency: use WAL mode, one writer, transaction boundaries around every settlement; if write lock timeout occurs return `503 retryable`. - Hash-chain corruption: `m2-ledger audit verify-chain` must fail closed and require admin intervention before new mutation if local tx hash continuity breaks. - Abuse/fake earnings: pricing evidence and ratings are catalog-owned; ledger can mark suspicious self-dealing metadata but does not curate. ## 7. [NEEDS CLARIFICATION] 1. Exact operator_id source of truth: derive from m2-gpt `agents.principal_ref`, m2o `fleet.json`, or a new marketplace operator registry? MVP can maintain an `accounts.operator_id` mapping, but the canonical owner should be chosen before implementation. 2. Forgejo audit repo name: use a dedicated `m2/market-audit` repo or S2's registry repo with `audit/ledger/...` paths. 3. Credit-to-USD conversion policy for route settlement: fixed operator-configured rate is enough technically, but the initial `m2cr_per_usd` value and who can change it need an operator decision.