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>
759 lines
26 KiB
Markdown
759 lines
26 KiB
Markdown
# 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 <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.
|