m2-market/specs/S1-ledger-economy.md
m2 (AI Agent) 8a8db9f2cc specs: m2-market spec discovery — 6 subsystem specs + synthesis index
herdr workforce output (6 codex angle-specialists -> claude synthesis):
S1 ledger/economy, S2 catalog/registry, S3 storefront (cargstore->M2 Store),
S4 Solution Scout (standalone watcher recommended), S5 packaging + 3 seed
Solutions (mm-pdf 25cr, agent-scaffold 35cr, competitor-scan 60cr),
S6 proposal engine + evidence loop.

SPEC-INDEX is the tie-break: 23-row cross-cutting contract table (16 mismatches
resolved, e.g. /v1 ledger paths win, m2store://listing/<id> deep link, one
m2.market.telemetry.v1 envelope, op_<slug> operator ids, tenant ids from m2-gpt),
8 deduped clarifications with defaults, 4-phase build order where the paid-install
wedge does NOT block on fedlearn (local ApplyAdapter until rails land), risk register.

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

26 KiB

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:

{
  "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

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

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:

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

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

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

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

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:

{
  "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:

{
  "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:

{
  "operator_id": "op_gunnar",
  "tenant_id": "m2",
  "amount": 100,
  "reason": "starter_grant_2026w27",
  "idempotency_key": "starter:op_gunnar:2026w27"
}

Response:

{
  "tx_group_id": "txg_01j...",
  "tx_ids": ["tx_01j..."],
  "balance": 100
}

Fixed install settlement

POST /v1/settlements/install

Requires ledger:install.

Request:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{"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:

{
  "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:

{
  "snapshot_id": "snap_2026-07-02",
  "path": "audit/ledger/2026/07/2026-07-02.json",
  "commit": "abc123...",
  "snapshot_hash": "sha256:..."
}

Snapshot 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:

{
  "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:

{
  "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 <listing_id>: 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.