You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
id: investigate: token issuance in the ID graph — per-ID issuance, issuer snapshots, authorized conversion rates #2041
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.
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.
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)
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.
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).
Burn — a transfer to the issuer (or a dedicated burn identity), reducing counted supply.
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.
Conversion receipt — {offer (specific version/frontier), both leg references}, emitted on execution (below).
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.
snapshot(currency) → latest signed snapshot object.
Abuse and failure cases
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.
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.
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.
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). ✅
Lying snapshot — anchored: contradicted by the ledger, mechanically. Graph-native: contradicted by recomputation. A snapshot is an auditable assertion, not a trust root. ✅
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)
Filed spec follow-on id: design: graph token object formats + snapshot anchoring spec (from #2041) #2251 — graph token object formats (definition/issuance/transfer/offer/receipt/snapshot wire shapes in pkgs/id), the snapshot anchoring join against TigerBeetle user_data_128, and the app-function API surface above — as a design spec (no permanent feature on an unreviewed investigation), enqueued as triage task 3406 in the same pass.
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').
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.
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.
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
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 under doc/ via PR; follow-ups + close.
Activity
developing-today commentedon Oct 29, 2026
CODE-962
dezren39 commentedon Oct 29, 2026
Parent epic: #2056 — ID platform expansion (2026-10-28 directive).
dezren39 commentedon Oct 29, 2026
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.
dezren39 commentedon Oct 29, 2026
🤖 Agent note · picked up
dezren39 commentedon Oct 29, 2026
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.mdin the program workspace) settled the ledger side: a currency is a bareu32ledger 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){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).{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.{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).{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.{offer (specific version/frontier), both leg references}, emitted on execution (below).{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
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
bindingfield chooses, once, at issuance.How snapshots anchor graph state to the ledger
A snapshot is a signed, checkable compression of state at a frontier:
user_data_128links; 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).Conversion execution
user_data_128on both legs → emits the conversion receipt object. Either both legs land or neither does.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, thenawardis 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
user_data_128cross-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.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)
pkgs/id), the snapshot anchoring join against TigerBeetleuser_data_128, and the app-function API surface above — as a design spec (no permanent feature on an unreviewed investigation), enqueued as triage task 3406 in the 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.
2 remaining items
dezren39 commentedon Oct 30, 2026
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').
dezren39 commentedon Oct 30, 2026
🤖 Agent note · picked up
doc: investigation — token issuance in the ID graph (#2041)
doc: anchored folds count only confirmed ledger executions (#2041, Co…
Merge pull request #2806 from developing-today/queue/3922-2041-token-…
dezren39 commentedon Oct 30, 2026
Investigation landed — closing
The wave-2 investigation/design document for this issue is landed on main:
doc/2026-10-30T19-18-00Z_investigation_token_issuance_id_graph/2026-10-30T19-18-00Z_investigation_token_issuance_id_graph.mdIt 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:
pkgs/idcurrency client API (gated on the id: design: graph token object formats + snapshot anchoring spec (from #2041) #2251 spec)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
doc/2026-10-30T19-18-00Z_investigation_token_issuance_id_graph/