Primitive catalog
The four reusable /p/ packages Cosmic Bull authored and deployed, and the realms that consume them.
Unlike the five pipeline applications, these were authored here. This repository is their source of truth — pearl/p/ and pearl/r/.
The API reference is the place to start
This file describes what each primitive is for, names its functions, and states its consumer contract and its limitations. It does not carry signatures.
Full signatures, doc comments, types and methods — generated from vm/qdoc on live pearl-1 and re-checkable with one command — are in catalog/api/:
| feeledger | 3 functions · 1 type · 13 methods | live $help |
| coinio | 3 functions | live $help |
| duebook | 2 functions · 2 types · 20 methods | live $help |
| permbook | 3 functions · 2 types · 18 methods | live $help |
The $help links are the live, executable view and include a generated gnokey maketx call command. Be aware of what they do not show: $help renders exported top-level functions only, so of the 67 exported symbols above it displays 11, and none of the methods or types. That is the gap catalog/api/ exists to close; the measurement is in DISCOVERY_APIDOCS.md §2.2.
On-chain discoverability
All four are registered in the live service_registry realm under service type library, so they can be found by anyone enumerating the chain rather than only by reading this file. The registrations were made on 2026-09-22; each is recorded with its transaction hash at pearl/DEPLOYMENT.md §16, and each is re-verified against the chain by tools/verify_catalog.py check C11.
ListByType("library") -> feeledger, coinio, duebook, permbook
No bytes were deployed. These are calls into a realm that was already live; the primitives' own [addpkg] heights are unchanged, which is the proof of that.
A registration is an attestation, not a proof. service_registry's INTEGRATOR CONTRACT applies in full — the realm does not and cannot verify that a registered pkgPath exists or that the registrant controls it. In particular, registration does not retire the "one consumer" limitations recorded below. Being listed is not being reused.
/p/ primitives (deployed, immutable)
feeledger
| Path | gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledger |
| Deployment tx | feb4b559aecd184494f4c0786732efce263626811edd4f4bacfb2b9563c1f2a0 |
| Height | 572441 |
| Source | pearl/p/feeledger/ |
| Depends on | gno.land/p/nt/avl/v0 |
| Consumed by | vault, bounties, coindemo, grants, market, bounty_panel, service_market, subscriptions |
Per-account balance ledger plus an explicit basis-point fee pot. Error-pure (every operation returns an error; the caller decides whether to abort the transaction), overflow-checked throughout.
Conservation contract: held == UsersTotal + FeesAccrued + surplus.
State is an object the consumer realm owns — /p/ packages are post-init frozen and hold no state of their own.
Consumer contract (the parts the package cannot enforce):
- Keep the
*Ledgerpointer unexported — the grc20 PrivateLedger rule. An exported pointer to private state hands out write authority. - Credit exactly on verified receipt; debit exactly before payout; panic on every ledger error (a returned error does not revert state).
- Account keys are
address.String()derived fromcur.Previous()at crossing boundaries only.
Discovery classification at build time. Closest found: p/g1n4pl…/bazaar/fee/v1 (pure bps fee calculation plus seller proceeds, two functions) — RELATED IMPLEMENTATION, overlapping only feeledger's FeeFor. Honest admission on the record: this was found in the retroactive review, not before the build. Had the full enumeration been run first, that calculation could have been imported. The discovery gate exists because of this miss.
Known limitation. The unexported-pointer rule is documented here and in pearl/INFRASTRUCTURE.md but not in-package. The deployed copy is frozen; this is recorded for a v1 successor.
coinio
| Path | gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coinio |
| Deployment tx | 31b53d1b763132576eb85fce25fb7d3881b279351d0949e18a72c366167706ae |
| Height | 583585 |
| Source | pearl/p/coinio/ |
| Depends on | stdlib only |
| Consumed by | coindemo, grants, market, bounty_panel, service_market, subscriptions |
Chain-facing coin plumbing, extracted after the same guard logic appeared in three realms. Pure and stateless; panic-only (no error returns — a coin movement that cannot be completed must revert the transaction).
| Function | Purpose |
|---|---|
Receive | The receipt-guard trio — verifies the caller shape, the envelope, and the denomination |
Payout | RealmSend from the calling realm's own address |
Sweep | Reserve-protected single-denom surplus extraction |
HeldAt | Bank balance read |
Consumer contract (the parts the package cannot enforce):
- Pass
0and your crossing entrypoint's own livecur. The(_ int, rlm realm, …)shape is required because/p/cannot declare crossing functions; every mover assertsrlm.IsCurrent(). Receiveis a read of the transaction envelope, not a consumption. Call it at most once per transaction and credit its result at most once.- Debit your accounting before
Payout/Sweep(checks-effects-interactions). A panic-abort reverts the debit and the send together. - Authorization is yours — gate the entrypoint before calling in. coinio moves coins; it does not decide who may.
- Coins can only move from the calling realm's own address.
Discovery classification at build time. Closest found: p/nt/treasury/v0 (a Banker interface, Payment objects, history and render) — RELATED IMPLEMENTATION. Inspected before the build and deliberately not reused: treasury abstracts multi-banker dispatch with canonical-implementation checks, while coinio is a minimal stateless guard-trio/payout/sweep contract, and treasury is marked "v0 Unaudited". Reason recorded at the time.
duebook
| Path | gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/duebook |
| Deployment tx | 5cd6de9e86a19cbf5633763dbf2536223a559212bdf51239304326e71f1b50bb |
| Height | 609571 |
| Source | pearl/p/duebook/ |
| Depends on | gno.land/p/nt/avl/v0 |
| Consumed by | duebook_demo |
Deferred-action scheduling. What feeledger is to money, duebook is to time: it holds no coins, performs no effects, imports no chain API, and owns no package-level state. The consuming realm owns a *Book, supplies the clock, and performs its own effect.
The one idea — Claim is consume-then-act. Claim(id, now) checks due / not-expired / still-open and removes the deferral before returning. The realm acts only after that point, so the deferral is already gone when the effect runs. Combined with monotonically allocated, never-reused IDs, at most one Claim per ID can ever succeed across all transactions, forever. Replay is not guarded against — it is structurally impossible. Because the realm performs the effect itself, no closure, callback, or capability ever crosses a realm boundary; duebook cannot be handed code to run, so it cannot be tricked into running the wrong code.
Lifecycle: open from Schedule until exactly one of Claim, Cancel (owner only), or Expire (anyone, once expired) consumes it. No other transition and no way back. Claimability is the half-open interval [DueAt, ExpiresAt); ttl == 0 means never expires. Consumed deferrals are removed, not archived — the ID counter, not a tombstone, is what prevents replay — so storage is bounded by maxOpen regardless of how many deferrals have ever existed.
Consumer contract (the parts the package cannot enforce):
- Supply a real clock. duebook cannot verify that
nowcame fromruntime.ChainHeight(). A realm that lets a caller choosenowhas no delay at all. The single most important obligation. - One clock, consistently — heights and seconds must not be mixed within a Book.
- Do not export the Book. A
*Bookis a mutable handle; returning one hands out the right to schedule, cancel and claim. (The feeledger PrivateLedger rule, restated.) - Authorize the actor. duebook authenticates nothing but ownership on
Cancel. Derive the caller fromcur.Previous().Address(). - Act after a successful
Claim, in the same transaction. Storing the authorization to act on later reintroduces the replay window this package exists to close.
All failures are returned as errors and leave the Book completely unchanged; Must* wrappers are the only functions that panic.
Discovery classification at build time (full evidence: pearl/p/duebook/DISCOVERY.md). Five sources enumerated before writing a line. EXACT DUPLICATE: none — no relevant existing implementation was found in the searched sources. RELATED IMPLEMENTATION, examined and not reused: Cosmic Bull's own live timelock_guardian (an /r/ product, not a capability; its Execute sets a flag and emits an event — it performs no effect, so a consumer that trusts it has gained nothing) and third-party r/g16m0r…/timelock (whose clock is a caller-supplied parameter). REUSABLE EXISTING PRIMITIVE, adopted as pattern precedent: p/moul/x/daily/ratelimit/v1 — live proof that a pure, time-aware /p/ with a caller-supplied tick is an accepted idiom on this chain; its float64 arithmetic was the one choice not adopted (duebook is integer-only). feeledger and coinio were reviewed and deliberately not used: duebook schedules actions, not payments, and pulling in a value primitive would add custody surface to a package that has none.
Known limitation — IterateOpen storage authority (audit Y1). The callback runs with the consumer realm's authority over duebook-allocated objects. Documented in-package and structurally narrowed (the iterator yields Deferral values, never pointers into state), but never exercised live — no cross-realm consumer exists yet.
permbook
| Path | gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/permbook |
| Deployment tx | 87866042519c18702b5e434c35284e70cfa5a8075c967a43d90382c2f225ccfb |
| Height | 612427 |
| Source | pearl/p/permbook/ |
| Depends on | gno.land/p/nt/groups/v0 (and errors) |
| Consumed by | permbook_demo |
Named permissions, granted to addresses and revoked, with an O(log P + log H) membership query. Like feeledger and duebook, it holds no coins, performs no effects, imports no chain API, and owns no package-level state. The consuming realm owns a *Book.
The one idea — authorization state that is the consumer's own. Discovery found that Cosmic Bull's live r/permission_registry already satisfies the literal objective, so the reason to build was narrow and specific: that realm's caps are global, shared and unraisable on an immutable realm (MaxResources = 1000, MaxResourcesPerAdmin = 20, MaxHoldersPerPermission = 200, no owner and no upgrade path in 526 lines). permbook inverts the ownership: each consumer allocates its own Book with its own Limits, so capacity contention across unrelated applications is structurally impossible rather than merely unlikely.
Two authorities, deliberately separate. Holding a permission out of a Book grants no power over the Book. Grant, Revoke, RevokeAll, DropPermission and the admin-handover calls all require the Book's admin; everything a holder can do is whatever the consumer gates on Has. This is what makes the capability safe to give an application that does not fully trust its own operators — and it was proven live, with the admin's own first Bump refused because the admin held no bump.
Admin handover is two-step (NominateAdmin → AcceptAdmin, with CancelNomination), so a typo cannot orphan a Book.
Bounds are per-Book and chosen at construction, clamped by hard ceilings the consumer cannot exceed: MaxPermissions (default 64, ceiling 256), MaxHoldersPerPermission (1024 / 10000), MaxNameLen (32 / 64). Names are restricted to [a-z0-9_], so a permission name can never carry markdown or address-shaped content into a consumer's Render.
Revoking the last holder prunes the permission. An empty bucket is removed rather than retained, so a Book's storage tracks live grants and not the history of grants — demonstrated live, where PermissionCount() returned 0 after the only holder of the only permission was revoked. The effect of an authorized call survives that revocation; the authority does not.
Consumer contract (the parts the package cannot enforce):
- Do not export the Book. A
*Bookis a mutable handle; returning one hands out the right to grant and revoke. (The feeledger PrivateLedger rule and duebook rule 3, restated — and the reason permbook never returns a*groups.Groupeither.) - Derive the caller from
cur.Previous().Address(), never from a parameter. permbook authenticates the principal it is handed; it cannot check where that address came from. Hasis a point-in-time answer. Gate at the moment of the effect, in the same transaction. Caching aHasresult reintroduces the window a revoke exists to close.- Choose limits for the application, not for comfort. The ceilings are a safety net, not a recommendation; a Book with 256 permissions makes
Permissions(addr)andHasAnyproportionally expensive. - Pick one admin model and keep it. permbook has exactly one admin per Book. A consumer that wants M-of-N must implement that itself and hold the Book behind it.
All failures are returned as errors and leave the Book completely unchanged.
Discovery classification at build time (full evidence: pearl/p/permbook/DISCOVERY.md). Five sources enumerated, including all 634 pearl-1 packages rather than a keyword sweep — which mattered, because p/samcrew/basedao is a live role-and-member store matching none of access|perm|rbac|role|auth|acl|grant. EXACT DUPLICATE: none — no relevant existing implementation was found in the searched sources. COSMIC BULL EXISTING IMPLEMENTATION, recommended and not extended: r/permission_registry (above). REUSABLE EXISTING PRIMITIVE, composed: gno.land/p/nt/groups/v0 — the address sets, the role registry, the ordered iteration and the readonly views all come from it, and permbook writes no container code of its own. RELATED IMPLEMENTATION, examined and not reused: p/samcrew/basedao (a DAO product, not a capability) and r/sys/*-style single-role registries, one of which documents that "each role can only have one assigned address."
Known limitation — one consumer. permbook is justified by being composable by multiple independent applications, and exactly one consumer exists. Until a second, unrelated realm imports it, its reusability is a design argument, not a demonstrated fact — the same caveat duebook carries.
Known limitation — dependency provenance. RESOLVED 2026-09-22; see the amendment below before relying on this paragraph. groups/v0 on pearl-1 imports p/moul/addrset and p/nt/bptree/v0; the local toolchain resolved a groups/v0 importing p/moul/addrset/v0, which does not exist on pearl-1 (see docs/TOOLCHAIN.md). The deployed bytes are byte-verified and the import resolved live at deploy time, but the dependency tree the tests ran against is not proven identical to the deployed one.
Amendment, 2026-09-22 — the limitation above is closed, and it named the wrong side. permbook's tested closure is proven byte-identical to pearl-1's: digest
bc0512a6def04b62on both sides, withp/moul/addrset,p/nt/avl/v0,p/nt/bptree/v0,p/nt/groups/v0andp/nt/markdown/sanitize/v0each byte-identical, and all four ofgroups/v0's compiled files matching exactly. The pinned cache holdsp/moul/addrset, notaddrset/v0— theaddrset/v0resolution is what the default (mainnet) remote serves, not what the pinned cache used. Full evidence: pearl/DEPLOYMENT.md, the correction at § "permbook's tested closure". The original text is kept per the record rules rather than deleted.
Pre-pipeline realms (deployed, immutable)
These predate the GitHub → Pearl pipeline. They were authored in this repository, and they are where the value-handling invariants were proven.
| Realm | Address | Tx | Height | Composes | Live validation |
|---|---|---|---|---|---|
r/…/vault | g1vfp5q3fkuv5pnefzvju6dlu7atulg59pl6264l | 829993dd958f49c13028db79cc12b907d452302d025a1c629357a00ffa5c8fb8 | 572482 | feeledger | 14 txs; H == U+F+S exact at every checkpoint (§1) |
r/…/bounties | g1x8fmlk48ucjzwslma92a2eetjqk0lp7afkrjqm | 1e67d09e07266d0a50bda463686fd8338d7b56adad333fdf6869d10685294f86 | 573203 | feeledger | 12 txs; H == B+U+F+S exact (§2) |
r/…/coindemo | g1x7rljdk7avp765cf2uxes9v0ysktwwp7rz54zu | 38aa65019a1a5dee3b3e8509498f7d9c52cdfa9b5fcca59bb4a05b5957c6908c | 583613 | coinio + feeledger | 8 txs; H == U+S exact (§4) |
r/…/grants | g10wjaak8z5nhzke76qxmsjdu2rjxcnm5j6zw54m | 6093675f15c27702f8c8599309e1a40e5964b96973369547ee11ba301f8a3f60 | 584731 | coinio + feeledger + sanitize/v0 | benchmark app; H == G+U+F+S (§5) |
r/…/market | g1y8nzwrv7ufe8c5r747vfpjm8jvayhgvddqwl2g | 76293576775be9db30ab097944990e9c5836d00e52a84b979d3284b7707f82d9 | 585791 | coinio + feeledger + avl + sanitize/v0 | multi-domain benchmark; 7 attack shapes rejected (§6) |
Validation helpers — not products
| Realm | Tx | Height | Purpose |
|---|---|---|---|
r/…/cointest | b19f57d705b450f1a022b1b6bb760b9015cb4cc4a72c491857d9ab5c53f7ccf6 | 572543 | out-of-band coin forwarding probe |
r/…/cointest2 | 7ce72ecd16ffb822d7e1a42aed7aaad573475d028321d309eb81b8a339be2eeb | 572561 | realm-issued denom probe (fully-qualified denom) |
Throwaway helpers used to attack the value-handling realms from outside. Never audited as products, and excluded from the portfolio count of 15 application realms (catalog/README.md).
Known divergences — deliberate, do not "reconcile"
- vault / bounties vNext. The local sources are refactored onto
coinio. The deployed v1s predate coinio and remain self-contained. Message-text deltas only, plus three recorded behavioral deltas in bounties from the 2026-09-20 audit round:Awardrejectswinner == realm address;BountyCancelledgained anamountattribute (an event schema change — indexers keyed to the v1 shape must handle both); and theRenderfee line was reworded to snapshot semantics. None is deployed. - coindemo's
Surplus()doc comment saysHeld() - UsersTotal()while the body usesledger.Liabilities(). Provably equal in this realm — the fee cap of 0 pinsFeesAccruedto 0 forever. Left as-is to preserve byte-custody with the deployed file.
Application-factory realms (deployed, immutable)
Authored here from an objective rather than ported from an existing repository. Same source-of-truth rule: pearl/r/.
| Realm | Address | Tx | Height | Composes | Live validation |
|---|---|---|---|---|---|
r/…/bounty_panel | g1cxukm68ntsaewu2qpd0x5a9xvmnpjx6fecgf4h | 2c2a8dc883d2a8c4d1318a3a9ec74a8184ec25579b5b3e4b4f28e291fcf33c79 | 608171 | coinio + feeledger | factory benchmark #1 (§12) |
r/…/duebook_demo | g1u0caqzv99d4p6878xwk287edjwdx8exf4whvvc | bbb5fc50464741a40e1d71bf449c67ccd000c41923b896d7c9bdb748b825af56 | 609644 | duebook | factory benchmark #2; 10 txs, exactly-once proven across transactions (§13) |
r/…/permbook_demo | g1qtt0yrnuv93xm2w2jg75khvxtypsp69pky0f3q | e98028e1add2ccba033d4538a2b04ea4a958b38a87e4eeb2ae57c1a402593731 | 612731 | permbook + sanitize/v0 | factory benchmark #3; 5 txs across 2 keys, gate proven to open and close across blocks (§14) |
r/…/service_market | g1fd72s0yvt0appn4grj4650nmp589lw27mf4me9 | 9f9d9f42b14fae53261f2e81c1862f90783651fbea74cc434702d2be55c37c99 | 628080 | coinio + feeledger + sanitize/v0 | factory benchmark #4; 11 txs across 2 keys, both orders driven terminal and the realm drained to 0ugnot (§15) |
r/…/subscriptions | g1fpqpq59lgy57w6083ryd56utc4ksvdhw38lezl | 60c23cd2d863e0e49553f936c6efcc52b62aa4b50f452b5feea4768528fd0cad | 639086 | coinio + feeledger + sanitize/v0 | factory build #5; 11 txs across 2 keys incl. two grace renewals and a permissionless expire, realm drained to 0ugnot (§17) |
service_market's and subscriptions' addresses are the two in this table read directly off the chain — vm/qeval on each realm's own Address() view, which returns the value init captured from unsafe.CurrentRealm(). They are therefore confirmed, not derived. See ../catalog/README.md on the derivation caveat that applies to the other rows.
service_market — the two-level state machine
The first factory realm whose novelty is a state machine rather than a capability. coinio and feeledger supply coin custody and balance accounting unchanged; what service_market adds is the offer/order split — a service is a reusable standing offer, an order is one purchase of it, and each order copies the provider address and fee bps in at purchase so nothing done to the service afterwards can redirect an in-flight payment.
The discovery gate's finding is what justified writing it: every escrow realm in the searched sources escrows funder-first (the party who will later decide escrows first, then solicits work), while a service marketplace must escrow customer-first against a standing offer. That inversion is why service_registry — a registry, not a marketplace — was classified RELATED and left untouched rather than extended.
Live-proven on chain: payment moved only on the customer's acceptance, the provider could not settle their own delivery, all three double-payment paths refused a terminal order, and the realm address ended at exactly 0ugnot. Not proven on chain: either timeout valve's success path, which needs 850 and 140,000 blocks respectively — the guards were verified live with real countdowns, the success paths by committed tests only.
duebook_demo — the experiment, not the demo
The name undersells it. A /p/ package's properties cannot be demonstrated on-chain by itself: vm/qeval is a single ephemeral evaluation, so a book constructed inside a query is discarded when the query returns, and "Claim succeeds exactly once across transactions" is precisely the claim that cannot be shown that way. duebook_demo exists to hold a Book in persistent realm state so the property can be attacked from real transactions.
It is a deferred-message board: Schedule(text, delay, ttl) reserves an ID; Publish(id) claims it and appends the text to a published list. Publish is deliberately permissionless — anyone may trigger someone else's due message, which is the point: the authorization was fixed at schedule time, and the primitive's one-shot guarantee must hold no matter who sends the later transaction. Cancel is owner-only; Expire is permissionless cleanup.
The result on-chain: deferral 1 published at height 609664, and the identical call in a later transaction refused with duebook: no such open deferral. Deferral 4 was scheduled by one key and published by a genuinely different one, and the original scheduler's later attempt on their own deferral was refused identically — exactly-once is caller-independent, not owner-scoped.
Guard ordering was proven from the error text, not merely from the abort: Publish(1) with send=1ugnot failed with this entrypoint does not accept coins rather than a Claim error, showing the stray-send rejection runs before the consume. A guard running after would have destroyed the deferral while rejecting the call, and a test that asserts only "it aborted" cannot tell the two apart.
Limitation. Every live call was an EOA MsgCall. duebook's cross-realm consumption path — and with it audit finding Y1 — has no live consumer and remains covered by local tests only.
permbook_demo — the experiment, not the demo
Same reasoning as duebook_demo, applied to a different unfalsifiable claim. permbook's central properties are cross-transactional (a revoke in transaction N closes the gate in transaction N+1) and cross-identity (a holder has no authority over the Book). vm/qeval is one ephemeral evaluation with a single caller, so neither can be shown by the /p/ alone. permbook_demo holds a Book in persistent realm state so both can be attacked from real transactions by real, separate keys.
It is a permission-gated bump counter and notice board: Bump requires the bump permission, SetNotice requires notice, and the admin entrypoints forward to the Book. Two permissions, two effects — so "holding A does not grant B" is testable rather than assumed.
The live sequence was run deny → grant → allow → revoke → deny, so both transitions are witnessed at real block boundaries. The opening denial is the load-bearing one: the caller was the Book's admin and was still refused, because administering a Book and holding a permission out of it are separate capacities. After the revoke, PermissionCount() returned 0 while Bumps() stayed at 1 — the effect of the authorized call outlived the authority that permitted it, which is the correct and non-obvious semantics for integrators.
Escalation was then attacked from a genuinely separate key holding only bump: Grant, NominateAdmin and SetNotice were all refused, the first two with permbook: caller is not the admin and the third with permission denied: notice.
Guard ordering was proven from the error text, as in duebook_demo: Bump with send=1000ugnot failed with this entrypoint does not accept coins even though the caller held no bump permission at all — so rejectStraySend demonstrably runs before the permission check, and a refusal never happens after taking custody of coins.
init fails closed: an invalid origin caller leaves book nil and the realm permanently inert, because nothing assigns it afterwards. That makes Ready() a deploy-time obligation rather than a convenience, and it was checked as the first post-deploy action (true).
Limitation. Every live call was an EOA MsgCall, so permbook's cross-realm composition path has no live consumer. The two-step admin handover was exercised only as a refusal; no nomination was broadcast, so AcceptAdmin and CancelNomination remain covered by local tests only.
Undeployed local work
| Package | Status |
|---|---|
p/tally | First vertical slice. Ordered string → int64 tally with a running total, backed by p/nt/avl/v0. Never deployed. Originally written against the older gno v1.1.0 API era, ported to the current pearl era in the 2026-09-22 repo-wide audit (commit 95d37c6); builds and tests green under the pinned toolchain. Discovery: a name collision with r/g1zwf26…/tally (a single Bump counter); no relevant existing keyed-score implementation was found in the searched sources. |
r/upvotes | First vertical slice. Topic upvote board consuming p/tally; one vote per address per topic. Never deployed. Ported to the current era in the same pass. |
Kept as project history. They are not part of the live portfolio; deploying them would additionally need a real namespace (their gno.land/*/cosmicbull/… module paths are placeholders) and the full pipeline.