Skip to content

heartbeat: issuer_mint seat-ad tag + reader (agent-issued ecash, stage 1) - #962

Open
maxy-player wants to merge 3 commits into
MakePrisms:mainfrom
maxy-player:feat/ecash-mutual-credit
Open

heartbeat: issuer_mint seat-ad tag + reader (agent-issued ecash, stage 1)#962
maxy-player wants to merge 3 commits into
MakePrisms:mainfrom
maxy-player:feat/ecash-mutual-credit

Conversation

@maxy-player

@maxy-player maxy-player commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Overview

A mint issues digital cash: you hand it money, it hands back tokens you spend elsewhere,
and it redeems them later. This lane lets a maxplayer agent run a mint of its own and issue tokens
that are an IOU for its own future work — no outside money involved. An agent that accepts
those tokens is extending the issuer credit. They are bearer tokens: whoever holds one can
spend it, so the holder can later hire the issuer and pay with it, and the issuer retires what
comes back. Two agents trading work for work, with tokens as the ledger.

This PR is the announcement layer and nothing else. It adds one optional tag to a seat's
existing heartbeat advertisement so a seat can state that it runs its own mint, and it teaches
every reader how to parse that statement. No wallet behaviour changes, no mint is installed or
contacted, and no production code writes the tag yet.

Where this PR sits — the three slices

Slice 1 — the announcement. This PR. A seat states its own mint and its issuance counters on
its kind-30340 advertisement; any reader can parse the claim, and a malformed or unbacked claim
reads as unstated rather than making the reader reject an otherwise payable seat. Protocol
document, reader, four tests. Behaviour unchanged.

Slice 2 — the wallet. Teaches the buyer's wallet that an issuer's mint has no Lightning, so the
cross-mint hop refuses it in both directions: no sats can flow into a self-issued currency and none
can flow out. Written, held local at 3f13a1c, unpushed, and being reshaped after the owner's
ruling that there is no "real mint"/"testmint" class — the seat's configured accepted list is the
whole decision.

Slice 3 — the sidecar. Not started. An optional stock cdk-mintd installed during
maxplayer seller setup, the seat's own mint URL added to its own accepted list, retire-on-receipt
of its own proofs, live counters published on this tag, and a local two-seller test where each
hires the other and pays in the other's currency. A written report follows it — an artifact, not a
slice.

The design, and whose it is

Settled by the owner in Discord thread 1544823194182226071 on 2 Sep 2026. The worker invented
none of it:

  • One mint per agent, run as an optional sidecar process.
  • The unit stays sat. One token is one sat of the issuer's work at its published rate, and the
    mint URL is the sole distinguisher between one agent's currency and another's.
  • No cap. Nothing anywhere limits how much an issuer may issue.
  • Bearer. Anyone holding an issuer's tokens may pay that issuer with them.
  • Accepting an issuer is a manual operator act — someone adds the mint URL to their own
    accepted list. Nothing derives that decision.
  • No sats in or out of an issuer's mint, and no fee.
  • The legal question is deferred to the humans; nothing in the code speaks to it.

What's in the diff

2 files, +339/−3.

  • crates/maxplayer-core/src/heartbeat.rs (+303/−3) — the tag constant, the IssuerMintAd reader
    (emit and parse), the two struct fields that carry it, and four tests.
  • docs/protocol-v1.md (+36/−0) — one row in the kind-30340 tag table and one new subsection.

The tag is ["issuer_mint", url, outstanding, retired, last_seen]: the seat's own mint URL, then
minted-minus-retired, retired, and the unix time the counters were read. The counters are the
issuer's own statement — the seat's signature proves who said them, not that they are true — and
no code path acts on them. They exist for a human deciding whether to extend credit.

How to review this in five minutes

  1. docs/protocol-v1.md, the #### Issuer mint subsection. What is being claimed, and the
    reader rules. Four positional values, all required.
  2. heartbeat.rs, IssuerMintAd::from_tags. How a claim is read. Wrong number of values, a
    counter that is not plain decimal digits, an empty URL, or a URL the seat does not list in its
    own accepted_mints all read as unstated — never as a rejection of the advertisement. An
    optional tag must not be able to take a working seat off the market.
  3. The four tests. One pins the exact wire bytes; three are negatives — absent, additive and
    invisible to an old reader, and 32 malformed shapes.

What this PR deliberately does not do

  • No cap, anywhere.
  • No payment or wallet logic; nothing about how anyone pays anyone.
  • No mint installed, started, or contacted. No network call to any mint.
  • No production writer of the tag — a seat cannot yet publish one.
  • No protocol version bump. Readers must ignore tags they do not recognise, so a new optional tag
    needs none.
  • The three sat unit gates are untouched, proven below by blob id.
  • No sats move. Nothing in this diff can spend anything.

Test evidence

The seven GitHub Actions checks pass on this head — Build & test (acp) · Build & test (default
features) · Build (no default features) · JS test suites (web/app, web/network) · Money-path tests ·
Release workflow gates · Test the full shipped feature combo (acp + wallet).

One red mark, and it is not a code failure. The eighth check is Vercel, reporting
"Authorization required to deploy." — a deploy-authorization request against this repository, not a
test result. The identical red sits on #955
and #954, both of which a human has already
merged. Nothing to fix here.

An independent local run. Independent run, by hearth, the forge's staffing seat, independently of the author: cargo test -p maxplayer --locked (.github/workflows/ci.yml:48) at a detached, clean checkout of e78b177157 passed, 0 failed, exit 0. Record: forge/v2/hearth/runs/hearth-run-record-e78b177-20260903T0138Z.md, 4,504 bytes, sha256 0e29598265f2e43d7c236db15ed6bf32ce7c91425573785d433be604da8ff86a. That is 1 of the 7 run: cargo test steps in ci.yml. This is NOT a CI-green claim; nothing here speaks for the six steps that were not run.

Provenance

Authored by the forge worker w-ecash-mutual-credit (commits signed w-ecash-mutual-credit <worker@forge.local>) under the shared GitHub identity maxy-player. Graded by the forge advisor from the diff (A–L PASS at lock e78b177). Worker report: forge/v2/worker/reports/w-ecash-mutual-credit-stage1.md, sha256 f92c886aa11a6497e929234d43ce906df9ec16be20b0a6961f2386f3e263ddb8.

Verification detail — commits, blob proofs, the cap census, per-test line citations

Commits (base upstream/main = 8c3bc9b; merge-base re-derived = 8c3bc9b)

  • 7547e5a — stage 0 (feasibility read, no code) + stage 1: the tag, the reader, the doc subsection, four tests.
  • 2446900cap REMOVED from the tag, the doc and the tests on the owner's order (Bob, 2 Sep 23:37Z: "no wrapper, no limit"). The tag went from five positional values to four; both index tables renumbered (outstanding 2, retired 3, last_seen 4). heartbeat.rs +8/−19, protocol-v1.md +6/−7.
  • e78b177 — one module-doc word, heartbeat.rs:288, FIVE → FOUR, matching the tag. +1/−1.

No amend, no rebase of this branch. git diff --shortstat 8c3bc9b e78b177: 2 files changed, 339 insertions(+), 3 deletions(-) — heartbeat.rs +303/−3, protocol-v1.md +36/−0.

Holds, measured on e78b177

The three sat gates are untouched — proven by blob id, not by diff.

  • crates/maxplayer-core/src/gateway.rs blob ac4df06e0ff8f001d1e4dedeea84fdb903a31141 at both 8c3bc9b and e78b177. Gate at :415, a literal string comparison: if unit != "sat" { (inside parse_offer).
  • crates/maxplayer-core/src/payment_wallet.rs blob a433b6c62b983ffecf7084c23b908c6f7f7056f4 at both. Two gates, both enum comparisons against CurrencyUnit::Sat, not string literals: :223 if unit != CurrencyUnit::Sat { (inside terms_for_offer) and :2079 if terms.unit != CurrencyUnit::Sat { (the seller receive check).

cap census (git grep -n -w cap e78b177 -- <file>; pattern proven on 7547e5a, where the same command finds 12 hits in heartbeat.rs and 3 in protocol-v1.md):

The four tests, with line citations

Four new tests in heartbeat.rs: issuer_mint_wire_shape_round_trips (:1490, exact 5-element wire vector — name plus four values — pinned and parsed back equal), an_absent_issuer_mint_tag_is_unstated_and_adds_nothing (:1529), issuer_mint_is_additive_beat_only_and_invisible_to_an_old_reader (:1546, stating the tag adds exactly one tag, every other tag byte-identical, the old-reader view unchanged, an unknown neighbour tag ignored, no path onto a kind-3402 claim), a_malformed_issuer_mint_tag_is_unstated_never_a_rejection (:1610, 32 malformed shapes all parse with issuer_mint == None; the field/index table at :1632 reads 2/3/4).

Follow-ups, named not done

  • Slice 2 — the wallet's issuer-mint class and the Lightning-hop refusal: written, held local,
    unpushed, under reshape after the owner's no-real-mint ruling. Not in this PR.
  • Slice 3 — the sidecar, retire-on-receipt, live counters, the two-seller local test, then the
    written report. Not started.

🤖 Generated with Claude Code

w-ecash-mutual-credit and others added 3 commits September 2, 2026 15:58
Stage 1 of the ecash mutual-credit build.

One new OPTIONAL tag on the seat announcement,
`["issuer_mint", url, cap, outstanding, retired, last_seen]`, carrying the
seat's own mint URL and its issuance counters. Emitted from `HeartbeatDraft`
(`with_issuer_mint`), parsed into `ParsedHeartbeat.issuer_mint`. Absent or
malformed reads as unstated, never a rejection. Announcement only, never on
the kind-3402 claim (protocol-v1 §4.2 "Issuer mint"). No version bump: a
reader MUST ignore unrecognised tags (§2.1).

No production publish path sets it yet; stage 3 wires live counters.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner decision (Bob, 2 Sep, "lets keep it simple for now - no wrapper, no
limit"): nothing enforces a ceiling on an issuer's outstanding tokens, so
the tag no longer declares one. The tag is positional and shrinks to four
values: `["issuer_mint", url, outstanding, retired, last_seen]`. Both
index tables (the doc's position table and the malformed-shape test's
field table) are renumbered: outstanding 3->2, retired 4->3, last_seen 5->4.

`IssuerMintAd` loses `cap_sats`; serializer, parser destructure, the
wire-shape fixture and the arity labels follow. The counters stay: they
are the only trust signal an operator reads before extending credit.
…the tag

The tag shrank to four values when `cap` was removed; one module-doc
sentence at the top of the section still said five. Doc only.
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the MakePrisms Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant