Skip to content

id: investigate: token issuance in the ID graph — per-ID issuance, issuer snapshots, authorized conversion rates #2041

Closed

Description

@dezren39

LIST 11b. Model issuance in ID terms: every ID can issue tokens under its own name ('this user said to make 100 tokens of itself and give them to this other ID'); a deployment like developing-today's uses a central issuer, or snapshots from an official issuer, instead of a full blockchain. Conversions are authorized per pair at issuer-set rates (ID 123 accepts ABC at 0.8123 per ABC; ABC sets its own reverse rate). Investigate: issuance/transfer objects in the graph, how snapshots anchor off-ledger state to the TigerBeetle ledger, and what a transaction-accepting function looks like for apps/games (award coins to winners, donors).

Program: Drew's 2026-10-28 directive (full text: hatch-personal memory/topics/id-program-2026-10-28.md).
Working pattern: parallel investigations, then the named synthesis issue merges them into a full design. Route guidance: Workhorse by default; Specialist/Summit for synthesis and ramification-heavy work. A result that says "done" is not the endpoint — the orchestrator reviews it and may commission another round. Deliverables: a markdown design/investigation doc (mermaid/SVG diagrams welcome) linked from this issue, plus follow-up issues for anything buildable. Build a proof of concept where the design calls for one, but do NOT build the permanent feature on top of an unreviewed PoC.

Activity

  1. added
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    on Oct 29, 2026
  2. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  3. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    Parent epic: #2056 — ID platform expansion (2026-10-28 directive).

  4. added
    priority/highImportant and time-sensitive
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  5. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    Top priority — ID program wave-1. Drew directed this be picked up in the next runs (2026-10-29): enqueued directly as triage task 3352 in repo-health-code-pilot at queue priority 20 (only breakage-class work at priority 10 outranks it). Wave-1 is the ungated investigations and designs; syntheses and PoCs stay gated on their named inputs. Investigation route: Workhorse.

  6. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  7. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  8. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    Investigation result — token issuance in the ID graph (#2041, queue task 3352)

    Verdict

    The model Drew sketched is coherent, and it splits cleanly into two token classes that share one object vocabulary. The completed TigerBeetle investigation (#2040, investigations/tigerbeetle.md in the program workspace) settled the ledger side: a currency is a bare u32 ledger there, minting is a transfer out of a designated issuer account, conversions are application-composed linked transfer pairs, and all identity, naming, rate, and authorization semantics live in a control-plane service that verifies signed ID-graph statements before submitting ledger transfers (TigerBeetle has no authentication at all). This investigation designs the graph side #2040 pointed at: the signed objects, how snapshots anchor them to the ledger, and the functions apps actually call.

    The key structural decision: token identity is (issuer ID, currency-definition hash) — never the name. Anyone can issue a token called "coins"; names are display/petname-layer. Two tokens with the same name from different issuers are different tokens, and every object below carries the full identity so confusion is impossible at the protocol layer (see abuse case 3).

    The objects (signed events in the graph, per-author chains as everything else in pkgs/id)

    1. Currency definition — {issuer, name/symbol, scale, supply policy: capped{max} | open, binding: graph-native | ledger-anchored{ledger id}}, signed by the issuer. Its hash + issuer = the currency ID. Scale is fixed here and generous (per id: investigate: TigerBeetle — multi-currency ledgers, conversions, per-coin vs balance model, 3-node cluster topology #2040: a token launched at scale 2 cannot later subdivide).
    2. Issuance — {currency, amount, recipient, memo}, signed by the issuer on the issuer's chain. There is no hidden mint: total issued is a fold over the issuer's issuance objects, so supply is publicly auditable by anyone holding the chain.
    3. Transfer — {currency, amount, to, sender's chain position}, signed by the sender. Balance (graph-native) = fold over a holder's inbound/outbound transfer objects. A transfer is an assertion by its signer; whether it counts depends on the sender's chain being unforked and funded at that position — the same validity/counting split as everything else in this program (id: investigate: trust/visibility model — servers accept all signed objects; views count only allowed writers #2030).
    4. Burn — a transfer to the issuer (or a dedicated burn identity), reducing counted supply.
    5. Rate offer — {from-currency, to-currency, rate (exact rational — 0.8123 is 8123/10000, never a float), per-tx and total caps, expiry, eligible takers: public | named set}, signed by the offerer (issuer or any liquidity holder). Each direction is a separate offer, exactly per Drew's example: ID 123's offer to accept ABC at 0.8123 says nothing about the reverse; ABC's issuer publishes their own reverse offer or doesn't.
    6. Conversion receipt — {offer (specific version/frontier), both leg references}, emitted on execution (below).
    7. Issuer snapshot — {currency, as-of frontier, total issued, total burned, balance root (hash over holder balances), ledger position when anchored}, signed by the issuer — or, in a central-issuer deployment, by the service ID. See next section.

    Two classes, one vocabulary

    • Graph-native tokens (games, casual/community currencies): issuance and transfers are graph objects only; balances are folds. They work fully offline and peer-to-peer. Their hard limit is real and stated plainly: offline double-spend cannot be prevented, only detected — a sender who signs two transfers of the same funds has forked their chain or overdrawn their fold, both provable from the objects, and acceptance policy (below) decides how much risk a recipient takes before anchor/sync.
    • Ledger-anchored tokens (developing-today's central-issuer deployment, anything redeemable for real services): the graph objects are the authorization and audit layer; TigerBeetle is the balance truth, exactly as id: investigate: TigerBeetle — multi-currency ledgers, conversions, per-coin vs balance model, 3-node cluster topology #2040 specified — the currency service verifies the signed graph object, then submits the ledger transfer with the graph object ID stamped in user_data_128, so the two records are permanently cross-linked and reconciliation is a join, not an investigation.

    An app does not choose per-transaction: the currency definition's binding field chooses, once, at issuance.

    How snapshots anchor graph state to the ledger

    A snapshot is a signed, checkable compression of state at a frontier:

    • For anchored currencies, the service computes the snapshot from TigerBeetle (account counters/history at a ledger position — id: investigate: TigerBeetle — multi-currency ledgers, conversions, per-coin vs balance model, 3-node cluster topology #2040 §6.3) and publishes it as a graph object. The graph fold of issuance/transfer intents and the ledger's executed transfers must agree up to the stamped user_data_128 links; a snapshot that disagrees with either is provably false, and which side is wrong is mechanically determinable (ledger wins for balances; the graph wins for who authorized what).
    • For graph-native currencies, anyone with the full object set can recompute the balance root; the issuer's snapshot is a convenience for light clients, and a lying snapshot is provable by recomputation.
    • What snapshots buy: a light client (phone, web, a game server) can trust balances and total supply from one signed object + spot checks instead of folding the whole graph or holding a TigerBeetle connection — this is Drew's "snapshots from an official issuer instead of a full blockchain," made precise: the snapshot replaces global consensus with a named signer whose assertions are cheap to audit.

    Conversion execution

    • Anchored path (atomic): taker cites a specific rate-offer version → service verifies the offer is live at the offerer's current chain frontier (not superseded/revoked, caps and expiry hold) → computes both leg amounts from the rational rate, rounding in the liquidity provider's favour with any spread as a separate auditable leg (per id: investigate: TigerBeetle — multi-currency ledgers, conversions, per-coin vs balance model, 3-node cluster topology #2040 §2) → submits the linked transfer pair with one conversion ID in user_data_128 on both legs → emits the conversion receipt object. Either both legs land or neither does.
    • Graph-native path (honestly non-atomic): a swap is two transfers bound by a bilateral swap-agreement object both parties sign first (the id: investigate: match-result attestation — mutual concession, witness adjudication, offline play #2037 attestation pattern). If one leg never appears, the agreement + the completed leg are a signed claim against the defaulter — enforceable by registries/reputation, not by the protocol. Any design that promises atomic offline swaps of independent chains is lying; this one doesn't.

    The app-facing functions (what a game/app actually calls)

    • define_currency(name, scale, supply_policy, binding) → currency ID (the definition object).
    • issue(currency, to, amount, memo) — issuer only. For games: prefer the prize-pool pattern — mint the season's supply once to the game's own pool account/identity, then award is a transfer, keeping supply fixed and auditable; mint-on-award is reserved for issuers whose policy is explicitly open.
    • award(currency, to, amount, memo{match/event ref}) — transfer from the operator's pool; the memo links the payout to its cause (e.g. a id: investigate: match-result attestation — mutual concession, witness adjudication, offline play #2037 match ID), so "coins to winners and donors" is one call with provenance.
    • pay(currency, to, amount, memo) / balance(currency, of) / supply(currency) — folds or ledger reads depending on binding; one API either way.
    • accept_payment(currency, predicate) → handler — the transaction-accepting function: the app registers {currency, min amount, memo pattern}; on a matching transfer the service/app verifies signature + sender chain unforked + funds (fold or ledger), then fires the app's effect (grant the service, ship the item, enable the bandwidth tier) and emits a signed receipt object. Refunds/revocations of what payments bought are id: investigate: tokens for real services — Steam-key grants, returns/refunds, revocation #2042's subject; the pending/hold pattern id: investigate: TigerBeetle — multi-currency ledgers, conversions, per-coin vs balance model, 3-node cluster topology #2040 flagged lives behind this same handler for real-service currencies.
    • convert(offer, amount) → conversion receipt (above).
    • snapshot(currency) → latest signed snapshot object.

    Abuse and failure cases

    1. Issuer secretly inflates past an announced cap — impossible to hide: issuance objects are the supply; over-cap issuance is visible to every folder. Anchored path: the service refuses to execute mints past the registered cap, so inflation requires the service's collusion, which the user_data_128 cross-links would expose. Graph-native: not preventable, always provable; counting views (id: investigate: trust/visibility model — servers accept all signed objects; views count only allowed writers #2030) refuse over-cap issuances. ⚠️ Provable ≠ prevented — stated so the synthesis doesn't oversell it.
    2. Offline double-spend — sender forks or overdraws; both transfers are signed evidence. Recipient policy tiers by amount: small = cash-risk acceptance; large = require a snapshot anchor or ledger binding before delivering value. ✅ Detection is total; prevention is policy, named as such.
    3. Name confusion / squatting — "coins" from a scammer vs "coins" from a game: different currency IDs (issuer + definition hash); wallets/UIs display issuer petname + name together and never merge balances across issuers. ✅ At the protocol layer; UI discipline is a requirement, not a nicety.
    4. Stale or revoked rate offer executed against the offerer — execution cites a specific offer version and the service checks the offerer's chain frontier at execution time; a revoked/superseded offer fails cleanly (atomic pair never starts). ✅
    5. Offerer advertises a rate they can't fund — conversions fail atomically when the liquidity account lacks balance (id: investigate: TigerBeetle — multi-currency ledgers, conversions, per-coin vs balance model, 3-node cluster topology #2040); failed attempts cost the offerer reputation (offers and failures are both on record) but never create half-converted state. ✅
    6. Lying snapshot — anchored: contradicted by the ledger, mechanically. Graph-native: contradicted by recomputation. A snapshot is an auditable assertion, not a trust root. ✅
    7. Issuer key compromise — the catastrophic case for any issuer system: attacker mints/converts as the issuer until the compromise is noticed. Mitigations available in this model: supply caps bound mint damage even with the key; issuer key rotation as a signed object (old key names successor; definitions issued pre-rotation keep their identity); registries/views pinning which issuer key generation they count. Full treatment belongs to id: design: ID currencies v1 #2043's synthesis — flagged, not solved here. ⚠️

    Not decided here (for the synthesis #2043 — which stays gated)

    #2043 also needs #2042 (tokens for real services: grants, refunds, revocation), which is still open and uninvestigated as of this pass — so the synthesis remains gated and this investigation does not pre-empt it. Also open for #2043: snapshot cadence and who signs in the developing-today deployment; issuer key-rotation mechanics (case 7); whether graph-native currencies get a shared fold/index service or stay purely client-side.

    Follow-on routing (same pass)

    🤖 Agent note · done — queue worker (repo-worker-code), lane C, task 3352. Investigation recorded here; spec follow-on filed + enqueued; synthesis #2043 remains gated on #2042.

  9. 2 remaining items

  10. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    Queued: repo-health-code-pilot task 3922 (2026-10-30), wave-2 follow-on. The 2026-10-29 triage verified this issue and recorded 'write the investigation' as the next step; this task is that write — investigation doc PR under doc/, follow-up issues for buildable pieces, close with the doc link. Ordered by Drew ('queue unrun').

  11. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 30, 2026
  12. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: queue worker (repo-worker-code), Lane D shift
    • Session / ID: queue task 3922
    • Found via: queue claim
    • Task: Write the wave-2 investigation/design doc for token issuance in the ID graph (per-ID issuance, issuer snapshots, authorized conversion rates) under doc/ via PR; follow-ups + close.
    • Expectation: effort m; done = doc PR opened/merged, follow-up issues filed, issue closed with doc link.
    • Links: task 3922
  13. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    Investigation landed — closing

    The wave-2 investigation/design document for this issue is landed on main:

    It formalises the wave-1 findings recorded on this issue (task 3352) and reconciles them with the designs that landed after them (#2062 control plane, #2066 centralised exception, grant lifecycle spec from #2042). It covers: the seven issuance/transfer graph objects; currency identity in three layers (issuer+definition / registry currency_id / ledger u32); the two bindings (graph-native folds vs ledger-anchored); the snapshot anchoring model (central issuer vs issuer snapshots vs full blockchain); how authorized per-pair conversion rates surface (engine = #2063, RateAuthorisation schema = #2290); the app-facing function surface incl. accept_payment; and both worked examples traced graph-to-ledger (§8: an ID issues 100 self-tokens to another ID; §9: a game awards coins to a winner and a donor). One Codex review finding on the PR (anchored folds must count only confirmed ledger executions, not signed intents) was fixed in the doc before merge.

    Follow-ups filed from the investigation:

    Hand-off to #2043 (doc §11): snapshot cadence/signer per deployment class; rate-offer vs RateAuthorisation as two projections of one authorization; graph-native fold/index service choice; acceptance risk-tier defaults. The spec successor #2251 remains open and is now unblocked on its parent investigation.

    🤖 Agent note · done

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

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)priority/highImportant and time-sensitive

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions