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>
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/jobevents 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:
100credits. - 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 onlym2cr. - 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_keyreturn 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 -> paidis 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.sqlite3M2_LEDGER_API_KEYS_FILE=/run/secrets/m2-ledger-keys.jsonM2_GPT_BASE_URL=https://gpt.machinemachine.aiM2_GPT_ADMIN_TOKENor m2-gpt service key for identity introspectionFORGEJO_URL=https://git.machinemachine.aiFORGEJO_ORG=m2FORGEJO_AUDIT_REPO=market-auditFORGEJO_TOKENPLATFORM_CUT_BPS=1000STARTER_GRANT_CREDITS=100
No secrets are committed to repos or images.
Auth and identity
MVP supports two accepted caller modes:
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.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_idonly for admin/service operations. Buyer identity for agent/user calls is server-derived from auth. - Cross-tenant reads return
404, not existence-revealing403, unless caller hasledger:admin. - Service keys are scoped:
ledger:install,ledger:job,ledger:route,ledger:grant,ledger:snapshot,ledger:admin. - All mutations require
Idempotency-Keyheader oridempotency_keybody 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), minimum1ifplatform_cut_bps > 0andamount > 1.seller_net = amount - platform_cut.- Rows:
- buyer -> seller,
seller_net,install - buyer -> platform,
platform_cut,install
- buyer -> seller,
- 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_funds409 duplicate_idempotency_key409 license_already_granted422 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_escrowrow withtx_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.createdgrant.starter_issuedinstall.settledinstall.refundedlicense.grantedjob.fundedjob.startedjob.milestone_submittedjob.milestone_releasedjob.cancelledjob.disputedroute.settledsnapshot.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 servem2-ledger migratem2-ledger account create --operator-id op_chris --tenant-id m2m2-ledger grant starter --operator-id op_chrism2-ledger balance op_chrism2-ledger tx list --operator-id op_chris --limit 20m2-ledger snapshot runm2-ledger audit verify-chain
m2-market integration verbs call the HTTP API, not SQLite:
m2-market wallet balancem2-market wallet txm2-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.tomlgenerated 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.jsonand~/.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/v1usingAuthorization: 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, andlicense_grantsinto an extended m2-gpt billing schema, preserving tx hashes and IDs. m2-ledger becomes a compatibility API or billing module. The append-onlyledger_txmaps to a futurebilling_ledger_entries;spend_logmaps to source metering;tenant_rollups_daily.extrascan 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/meandGET /v1/balances/{account_or_operator_id}before purchase display.POST /v1/settlements/installon 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_idand optionalproposal_id. - If install accepted, Store/CLI includes
proposal_idin install settlement metadata so ledger events can feed conversion evidence.
S5 Packaging / runtime-sync
- Install order:
- Resolve listing and permissions.
- Settle ledger install.
- Receive license grant.
- Apply via runtime-sync /
m2-core-sync. - On apply failure, call refund/compensation endpoint if no durable install state was written.
- Apply path must check grant via
GET /v1/licenses/...and writegrant_idinto 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_idmust be explicit; shared market evidence usestenant_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:
- MVP internal credits in
m2-ledger. - Add exportable hash chain and deterministic snapshots from day one.
- When m2-gpt billing grows a first-class billing schema, migrate ledger tables under m2-gpt while preserving IDs and hashes.
- 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
404for 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=trueadmin/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-chainmust 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]
- Exact operator_id source of truth: derive from m2-gpt
agents.principal_ref, m2ofleet.json, or a new marketplace operator registry? MVP can maintain anaccounts.operator_idmapping, but the canonical owner should be chosen before implementation. - Forgejo audit repo name: use a dedicated
m2/market-auditrepo or S2's registry repo withaudit/ledger/...paths. - Credit-to-USD conversion policy for route settlement: fixed operator-configured rate is enough technically, but the initial
m2cr_per_usdvalue and who can change it need an operator decision.