Application catalog
The five applications completed through the GitHub → Pearl pipeline.
All deployed on pearl-1, under the namespace gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/…, signed by the agent key g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3.
None of the five holds funds. All reject coins attached by a user call, none imports chain/banker, and every realm address is exists:false / coins "0".
Summary
| # | Application | Category | Deployed SHA | Height | Findings resolved |
|---|---|---|---|---|---|
| 1 | fee_split | Revenue & value flow | 819239a | 596803 | 0 RED / 3 YELLOW |
| 2 | timelock_guardian | Security / delayed execution | bb05e64 | 602213 | 0 RED / 0 YELLOW at final round (6 fixed in round 1) |
| 3 | upgrade_registry | Upgrade tracking | 2d5a748 | 602876 | 0 RED / 5 YELLOW |
| 4 | permission_registry | Access control | e3d6aea | 604615 | 1 RED / 6 YELLOW |
| 5 | service_registry | Discovery | 3c598d2 | 605546 | 1 RED / 7 YELLOW |
1 — fee_split
| Repository | https://github.com/SillyZir/fee_split |
| Deployed SHA | 819239ad5ddc46cf685f5ff363cabb7290848c1f |
| Category | Revenue & value flow |
| Realm path | gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/fee_split |
| Realm address | g12v97h3jexeacya6r4pkrm722axgahet25e6lms |
| Deployment tx | 061c89b1fd34f696396a2ee4e0b39dc7a808cdb0e5916763d39a7afe80c43c16 |
| Height | 596803 |
| Record | pearl/DEPLOYMENT.md §7 |
| Remediation layer | pearl/r/feesplit/FIXES.md |
Architecture. Named splits, each with recipients holding basis-point shares. Deposits are split per the shares at deposit time; recipients claim their accrued balance. A protocol fee switch defaults to zero and is immutably capped at 1%, with the fee snapshotted at deposit so an admin cannot retroactively re-price a deposit. Lifecycle: active → frozen → archived, with both value paths refusing an archived split.
Reused primitives. None — stdlib only. The upstream application is self-contained, and adding dependencies would have changed it rather than ported it.
Discovery classification. 626 packages enumerated on pearl-1. splitpayment (string-identity, no custody) and moul/splitter/v1 (accounting-only) — both RELATED IMPLEMENTATION, neither suitable. No exact duplicate found in the searched sources.
Live verification — PASS. CreateSplit (hunter/keeper 7000/3000); deposit at 0 bps with bank 100000 == 70000+30000+0 exact; claim with the recipient's balance delta matching exactly; SetFee(100bps); deposit at 100 bps with bank 80000 == 34650+44850+500 exact. Full drain to bank "0". Lifecycle proven: Freeze → deposit rejected "split is frozen"; Archive → deposit rejected "split is archived: split_1".
Adversarial — 6 rejections live: non-admin SetFee; non-recipient Claim; stray-send Claim; SetFee(101) over the cap; non-admin UpdateShares; coinless Deposit.
Invariants. Per-split sum(balances) + TotalClaimed == TotalDeposited held at every checkpoint (149500 == 0 + 149500 final). Realm-level bank == Σ claimables + feesAccrued verified against auth/accounts at three checkpoints, all exact.
Source provenance. The only application in the series where the deployed bytes are not byte-identical to the GitHub file. Transformation was T1 + T2: namespace rewrite plus a mechanical API-era port (chain/runtime.* → chain/runtime/unsafe, NewBanker(bt) → NewBanker(bt, cur), test-side cross → cross(cur) and 3-arg → 4-arg uassert). Every substitution is occurrence-pinned in pearl/r/feesplit/port.py. The deployed bytes were verified byte-identical to a re-run of port.py over the GitHub commit, and to the workspace commit.
Limitations.
- ⚠️ The repository has moved past the deployed commit. HEAD is now
e5aadfebc890c21ade4076e1d75c9c0e0dd77809, a pushed security remediation (runtime-current identity, guard-coverage tests, pearl-native stdlib). It is not deployed. The live v1 is frozen at bytes derived from819239a, and nofee_split_v2has been authorized. The local working treepearl/r/feesplit/fee_split.gno(sha256a90db685…) matches the undeployed HEAD, not the live realm (e2198ebb…). This divergence is deliberate — do not "reconcile" it by redeploying. - The deployed v1's fee admin is the hot agent key. A testnet-only posture. Nominating a user-controlled backup admin through the two-step handover requires an address from the user. Open item.
- The
MsgRun-shape refusal is not constructible in the test driver (testing.SetRealmaccepts only user and/r/realms) and is documented at the test rather than exercised. - The
init()feeAdmincapture is untestable in the test VM and was verified live (GetFeeInfoadmin == deployer). - No committed
DISCOVERY.md— discovery was performed and is recorded in §7 of the deployment record. The committed-discovery-record format was established later, at Application #5.
2 — timelock_guardian
| Repository | https://github.com/SillyZir/timelock_guardian |
| Deployed SHA | bb05e64898967125b900a9275ca84229bff5e007 (= current HEAD) |
| Category | Security / delayed execution |
| Realm path | gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/timelock_guardian |
| Realm address | g1w06mqv4h53vtfgfeyz2eglplpwfpytcqqqjwkh |
| Deployment tx | 6db3f74c764523f8ea1d82f7cd419639a8bb597494b170a8d2ea56bc8779efbb |
| Height | 602213 |
| Record | pearl/DEPLOYMENT.md §8 |
| Remediation layer | pearl/r/timelock_guardian/FIXES.md |
Architecture. Targets, each with an owner and a guardian and a minimum delay. The owner schedules actions with a delay at or above the target's minimum; anyone may execute once the delay elapses (permissionless execution, so liveness does not depend on the owner); the guardian may veto; the owner may cancel. An action has a ready window and a 30-day grace period, after which anyone may Expire it. Two-step target ownership transfer.
Reused primitives. None — stdlib only.
Discovery classification. Performed; no committed DISCOVERY.md (the format was established at #5).
Live verification — PASS. RegisterTarget (owner=default, guardian=keeper, min 60s); schedule with execute-too-early rejected ("too early: 50s remaining" — integer seconds, a round-1 fix visible live); execution by hunter, proving the anyone-executes property; double-execute rejected; guardian veto with reap proven ("action not found"); owner cancel; two-step ownership with a wrong-acceptor rejected, and the original creator rejected from Schedule after transfer.
HTML-injection probe (Y3 escalation check). An action carrying <script> / <img onerror> / [link] payloads was scheduled live, then the rendered output was inspected through both the render API and raw gnoweb HTML: fully neutralized to (script)alert(1)(/script)…, with zero raw hostile tags in the served page. Y3 settled SAFE on live evidence.
Adversarial — 8 rejections live: foreign Schedule; foreign Cancel; non-guardian Veto; duplicate register; sub-floor minDelay; below-target-min delay; guardian-strip while pending; coin-attached Schedule ("this realm does not accept coins").
Invariants. Realm address exists:false / coins "0" at every checkpoint. Every abort message byte-matched the audited source.
Source provenance. T1 (gnomod-only) — the deployed .gno is byte-identical to the GitHub commit. Verified against the workspace and a fresh clone. 50/50 tests, lint clean, three audit rounds ending 0 RED / 0 YELLOW.
Limitations.
- The
Expiresuccess path is not reachable live. It needs 30 real days past readiness (the grace period). The guard matrix was exercised live (live action → "action is not expired"; executed → "action already executed"; ghost → "action not found"), but the success path is pinned only byTestExpire_PermissionlessRecoveryandTestVictimRecoversBehindSybilWallin the deployed-identical suite.
3 — upgrade_registry
| Repository | https://github.com/SillyZir/upgrade_registry |
| Deployed SHA | 2d5a74870b08d70ff8e56e6e1efbace1803dd3bd (= current HEAD; base 9d22f7f) |
| Category | Upgrade tracking |
| Realm path | gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/upgrade_registry |
| Realm address | g1g453tcmapqn6hj9zlydfz3mt3vxh8gq5vwv8sg |
| Deployment tx | 532be90ab90521b0d6d4b3f983ede9dd315c8975cf30ad5c8984837f4d57e34d |
| Height | 602876 |
| Record | pearl/DEPLOYMENT.md §9 |
| Remediation layer | pearl/r/upgrade_registry/FIXES.md |
Architecture. Contract entries keyed by address, registered self-provingly — Register records the caller's own address as the proof of control — with an owner nomination that the nominee must accept. Deprecate(old, new) records a successor, gated by a successor-consent check: the successor entry must be owned by the caller, so provenance cannot be forged. GetLatest walks the migration chain to its end; GetMigrationChain renders it with [deprecated] tags.
Reused primitives. None — stdlib only.
Discovery classification. Performed; no committed DISCOVERY.md. Later classified from the other side: service_registry's discovery record grades this realm COSMIC BULL EXISTING PRIMITIVE — complementary, not overlapping (address-keyed migration chains vs name-keyed path resolution; the two compose, neither subsumes).
Live verification — PASS. Register demo_vault_v1 by hunter and demo_vault_v2 by keeper, both nominating default; AcceptOwnership on both by default; Deprecate v1 → v2. Reads confirmed: GetLatest(v1) == v2, GetMigrationChain correct with the [deprecated] tag, GetInfo correct, GetOwnerContracts(default) returning both entries, and Render showing v2 Active / v1 Deprecated with successor.
Adversarial — 8 rejections live: pre-acceptance Deprecate; successor-consent forgery ("successor entry is not owned by the caller"); wrong-acceptor AcceptOwnership; self successor; unregistered successor; duplicate Register; invalid name; coin-attached Register.
Invariants. Realm address exists:false / coins "0" at every checkpoint.
Source provenance. T1 (gnomod-only) — deployed .gno byte-identical to the GitHub commit, verified against workspace and fresh clone. Pipeline: mechanical port (19/19) → audit (0 RED / 5 YELLOW) → remediation (all 5 fixed, 8 gap tests added → 27/27, verification audit 0R/0Y) → push → fresh-clone verification (27/27 from GitHub bytes) → deploy.
Limitations.
GetLatestis O(chain length). Documented as an explicit COST WARNING for on-chain integrators rather than bounded — bounding it would change the semantics.- Frozen-entry wedge. A self-managed entry whose registrant loses their key can never be updated. This is a documented design limitation, not a vulnerability — only the owner can cause it, to themselves.
Registercarries a warning and guidance to nominate a manager frominit(). GetInfo's abort path is excluded from the crossing-path tests: it is a same-realm panic, whichrevive-based abort assertions cannot catch.
4 — permission_registry
| Repository | https://github.com/SillyZir/permission_registry |
| Deployed SHA | e3d6aeaf3571ec35c2af68c5fbb756480b76ed03 (= current HEAD; base facb9f0) |
| Category | Access control |
| Realm path | gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/permission_registry |
| Realm address | g1x9tatmqcdlas2w6k724mrs2zcgh3awf3s5wkhk |
| Deployment tx | a30c09427173a45553a9befcac38d410aa0e416eb785145942ee5378d6a8c54f |
| Height | 604615 |
| Source sha256 | 603fec571a55d051353e101c64c0490a5268124abda8d88ac703b9bbf26fb763 (18070 B) |
| Record | pearl/DEPLOYMENT.md §10 |
| Remediation layer | pearl/r/permission_registry/FIXES.md |
Architecture. A multi-tenant ACL: resource → permission → holder. Each resource has an admin (the creator); the admin grants and revokes named permissions to addresses; Has(resource, permission, holder) is the read that consumer realms call. Two-step admin transfer with nominee consent. Emptied permissions are pruned from the permission list. Deleted resource names enter a 90-day reservation that preserves the original creator's reclaim right.
Reused primitives. None — stdlib only.
Discovery classification. Performed; no committed DISCOVERY.md. Later classified from service_registry's record as COSMIC BULL EXISTING PRIMITIVE — pattern source, not dependency: no shared state, but its six audited remediation shapes became the template for #5.
Findings — 1 RED / 6 YELLOW, all resolved.
| # | Finding | Resolution |
|---|---|---|
| R1 | Namespace monopolization | MaxResourcesPerAdmin = 20 + O(1) adminResources counter; MaxResources 200 → 1000 |
| Y1 | Latent Class-2 caller identity | all 8 entrypoints take cur realm; caller() helper deleted |
| Y3 | Unbounded Render | bounded to 20 resources / 8 permissions |
| Y4 | One-step TransferAdmin bricks a resource | two-step with nominee consent + cancel; quota checked at consent |
| Y5 | No stray-send guard | rejectStraySend(cur) on all 8 entrypoints |
| Y6 | Declared bounds with no test coverage | cap tests added, including that pruning an emptied permission frees a slot |
| Y7 | Undocumented integrator contract on Has | explicit INTEGRATOR CONTRACT block |
Assessed and NOT changed: expired-tombstone growth, graded INFORMATIONAL with reasoning recorded — retired is never iterated, and the cost falls on the caller's own storage deposit. It was not downgraded to reach deployment.
Live verification — PASS. CreateResource with GetAdmin == default (proving cur.Previous().Address() live); Grant writer → hunter with Has(…)==true and Has(… keeper)==false; two-step admin transfer to keeper with the nomination proven inert (GetAdmin still default, GetPendingAdmin == keeper) then accepted, with the pre-existing grant surviving the handoff; a squat/reclaim sequence proving the creator's reclaim right survives a refused squat attempt; Revoke leaving GetPermissions == "none" and Render showing "No permissions defined" — live proof that the emptied permission was pruned.
Adversarial — 7 rejections live: non-admin Grant / DeleteResource / TransferAdmin by a permission holder (holding a permission confers no admin rights); consent forgery by a non-nominee; former admin Grant after handoff (authority moved, not copied — and the deployer holds no residual privilege); coin-attached CreateResource; markdown-injection resource name evil|](http://x)` rejected by the charset rule.
Invariants. Realm address exists:false / coins "0" at every checkpoint, including after the stray-send attempt. Siblings re-read and unchanged.
Source provenance. T1 (gnomod-only) — deployed .gno byte-identical to the GitHub commit. port.py is neutered (sys.exit): it pre-dates the remediation layer and re-running it would silently revert the fixes.
Limitations.
- Bounds not exercised live (economically impractical — each needs hundreds of funded transactions), correlated to committed tests:
MaxResources=1000(TestCreate_GlobalCap),MaxResourcesPerAdmin=20(TestCreate_PerAdminCap),MaxPermissionsPerResource=50(TestGrant_PermissionCap),MaxHoldersPerPermission=200(TestGrant_HolderCap),Rendertruncation (TestRender_Bounded), 90-day reservation expiry (TestReservation_ExpiresAndFreesTheName, viatesting.SkipHeights). - The R1 fix raises monopolization from one funded key to N funded keys. It does not make it impossible. Stated in
FIXES.mdrather than implied away. - The audit was run inline after the auditor subagent hit its usage quota — recorded in §10 rather than presented as an independent review.
5 — service_registry
| Repository | https://github.com/SillyZir/service_registry |
| Deployed SHA | 3c598d2e68f31bcd9697fc702f2f78703c4988c7 (= current HEAD; base 8f77853) |
| Category | Discovery |
| Realm path | gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/service_registry |
| Realm address | g1hv2mj29cyr2z94358m5fakj2g87zaf4yhrqud2 |
| Deployment tx | 60fc44a47fb8c38ca117609076d13b2dc1e75c407b1d70a70830fecf1fae30ba |
| Height | 605546 |
| Source sha256 | ead72c9ea14a6358a1d259177483e91a4f048f1a85050a803c3828d0531ee996 (19781 B) |
| Cost | gas_used 33,712,081 · fee 101,138 ugnot · 3,053,000 ugnot refundable storage escrow |
| Record | pearl/DEPLOYMENT.md §11 |
| Remediation layer | pearl/r/service_registry/FIXES.md |
| Discovery record | DISCOVERY.md, committed in the application repository |
Architecture. A global name → package-path directory. A name maps to one service record: owner, gno package path, type, description, metadata. Resolve(name) returns the path (aborting if absent); TryResolve is the non-panicking variant a consumer realm needs to avoid bricking itself inline. Ownership moves by two-step nominate/accept with cancel, with the per-owner quota checked at consent. Deregistration puts the name into a 90-day reservation held for the original registrant. Bounds: MaxServices = 1000, MaxServicesPerOwner = 20, MaxRenderServices = 25.
Reused primitives. gno.land/p/nt/markdown/sanitize/v0 — adopted as the Y5 security fix, replacing a hand-rolled four-substitution sanitizer. The on-chain copy was verified live before deployment against the local GNOROOT copy the tests compiled against.
Discovery classification. The first application to ship a committed DISCOVERY.md. Pearl-1 enumerated exhaustively (680 paths); 12 related implementations found and individually classified. Highlights:
| Implementation | Classification | Decision |
|---|---|---|
r/docs/registry | RELATED — closest match | Not reused: keys owner:name, so a global Resolve("name") is not expressible; Endpoint is unvalidated free text; quarantined and not on pearl-1 |
r/demo/defi/grc20reg | RELATED | Not reused (GRC20-specific). Its self-proving registration was adopted as the benchmark this application is audited against — the gap became finding Y6 rather than being glossed over |
ns* suite (7 realms, 34 paths) | RELATED — largest neighbor | Not reused: resolves name → owner+profile, not name → package path; writes gated by a third party's assertIsTrustedLogic with external admin/recovery/guardian keys; paid expiring NFT namespace vs free permanent directory. Recorded as the honest answer to "does a name service already exist on pearl-1": yes, and it is substantial — it answers a different question. |
r/sys/names, r/sys/users | RELATED (system layer) | Not reused, and deliberately not depended on. Noted for the audit: passing this realm's pkgpath validation grants nothing on-chain |
r/g1mjc0v90…/name_service | RELATED (name collision) | Not reused — Register(name, owner string) is the designation-forgery shape |
r/samcrew/agent_registry_v2 | RELATED | Not reused (off-chain endpoints, payments, reviews). Its MaxAgentsPerCreator and ReviewRenderMax are independent ecosystem confirmation of the two bounds added in #4 |
p/nt/markdown/sanitize/v0 | REUSABLE EXISTING PRIMITIVE | REUSED |
Also recorded, for completeness and not treated as prior art: a GitHub repository of the same name whose first three commits are authored by SillyZir with identical commit messages — a detached copy of this application's own lineage, not an independent implementation.
Findings — 1 RED / 7 YELLOW, all resolved.
| # | Finding | Resolution |
|---|---|---|
| R1 | Namespace monopolization | MaxServicesPerOwner = 20 + O(1) zero-free ownerServices index |
| Y1 | Latent Class-2 caller identity | all 6 entrypoints take cur realm; caller() helper deleted |
| Y2 | No stray-send guard | rejectStraySend(cur) first statement of all 6 entrypoints |
| Y3 | Unbounded Render | 25 rows + explicit truncation notice |
| Y4 | One-step TransferOwnership bricks an entry — and since a bricked entry can never be deregistered, permanently holes the name | two-step nominate/accept + cancel; quota checked at consent |
| Y5 | Hand-rolled sanitizer permits markdown link/image phishing | adopted p/nt/markdown/sanitize/v0 |
| Y6 | pkgPath is an unverified claim, no integrator contract | see below |
| Y7 | Silent repointing unobservable | ServiceUpdated now carries oldpkgpath and pkgpath |
The Y6 scope refusal — on the record
Y6 was not taken to its maximal fix, deliberately. Requiring self-proving registration (as grc20reg does) would mean only realms — never their human operators — could ever register a name. That is a fundamentally different application, and therefore a hard-boundary stop, not a routine in-scope fix.
Shipped instead, proportionate: a three-point INTEGRATOR CONTRACT carried verbatim on Resolve, in SPEC.md, and in README.md — attestation-not-proof / name-is-not-authorization / target-can-change — plus the Y7 event change that makes a repoint observable.
A reviewer may disagree with this call. The reasoning and the alternative are both written down so they can disagree on evidence.
Live verification — PASS. Empty-state reads clean (ServiceCount() == (0, 1000), Render == "No services registered."). RegisterService fee_split with owner derived from cur.Previous().Address(); description rendered pull\-based and metadata basis\_points, proving the on-chain sanitizer resolves and escapes live; OwnerServiceCount(default) == (1, 20). Two-step handoff default → keeper with the nomination proven inert, then accepted — and the quota index moving atomically and zero-free (the default key deleted, not left at 0). Return handoff keeper → default proving the two-step path in both directions and cleaning up the live test artifact. Final live state: 1 entry, fee_split, owned by default, no nomination pending.
Adversarial — 9 rejections live: coin-attached RegisterService (on an otherwise-valid call, so the coin guard was the only possible cause); pkgpath traversal gno.land/r/demo/../../evil; uppercase name; duplicate name; self-nomination; accept with no nomination open; consent forgery by a non-nominee; hostile repoint by a non-owner; and — after the handoff — UpdateService and Deregister by the former owner who is also the original deployer, both refused. Authority moved, it did not copy, and the deployer holds no residual privilege over an entry it created. Read-path: Resolve("does_not_exist") aborts while TryResolve returns ("", false).
Invariants. Realm address exists:false / coins "0" after the stray-send attempt and all writes; deployed bytes import no chain/banker. Siblings untouched — [addpkg] heights re-read unchanged (596803 / 602213 / 602876 / 604615). Namespace went 13 → 14 packages: exactly one added.
Source provenance. T1 (gnomod-only) — deployed .gno byte-identical to the GitHub commit. Port fidelity re-verified independently of port.py by a fresh script reading the upstream bytes directly: all eight substitution counts pinned exactly (1, 1, 1, 12, 44, 10, 11, 2) with 0 OriginCaller occurrences upstream. The mechanical port was green 21/21 before remediation, so every later failure is attributable to the remediation layer alone; 31/31 after. port.py neutered (sys.exit, exit code 1 verified).
The deploy payload was built from a fresh clone, cmp-verified against the pearl tree, and the addpkg body string was round-tripped through a file and cmp'd before broadcast to prove the transcription was byte-exact. On-chain file list is exactly [gnomod.toml, service_registry.gno] — no test file leaked.
Limitations.
- Second-identity coverage limit, stated plainly. Key generation was blocked by the Claude Code auto-mode permission classifier during this run, so no new probe key could be created. The pre-existing
hunterandkeeperkeys were used instead, which sufficed for every access-control case. No workaround viagnokey, shell signing, or key-file access was attempted. - Not exercised live, correlated to committed tests (economically impractical — each needs dozens to hundreds of funded transactions, and the faucet grant for the signing address was already drawn for the window):
MaxServices=1000(TestRegister_GlobalCap),MaxServicesPerOwner=20(TestRegister_PerOwnerQuota), quota-checked-at-consent (TestTransferOwnership_QuotaCheckedAtConsent),Rendertruncation notice (TestRender_Bounded, 26 entries), markdown link-injection into a table cell (TestRender_RejectsMarkdownLinkInjection— the live sanitizer itself is proven above, but no hostile entry was planted in a shared live registry), 90-day reservation expiry and lapsed-tombstone reclaim (TestReservation_*viatesting.SkipHeights). - Y6's residual is by design: a registered
pkgPathis an attestation, not a proof. Integrators must read the contract. - The R1 fix raises monopolization from one funded key to N funded keys.
Two test-authoring bugs, recorded as reusable knowledge
Found during #5, applicable to any Gno test suite:
testing.SetRealmis FRAME-SCOPED — an identity set inside a helper does not survive that helper's return. This silently ran a quota test as the empty address.sanitize.InlineTextescapes.— so a hostname never appears verbatim in rendered output, and assertions pinned against raw text fail.
Both are in docs/TOOLCHAIN.md.
subscriptions — multi-provider subscription hub
Live: gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/subscriptions — deploy tx 60c23cd2d863e0e49553f936c6efcc52b62aa4b50f452b5feea4768528fd0cad, height 639086, realm address g1fpqpq59lgy57w6083ryd56utc4ksvdhw38lezl (chain-confirmed via Address()). Application-factory build #5, objective: provider-created plans, per-period subscriber payment, tracked state and billing periods, cancellation, and structural prevention of unauthorized, duplicate and out-of-window payments, with obligations explicit on chain and deterministic renewal/expiration/cancellation rules.
Shape. One realm composing coinio + feeledger (strategy 1 reuse; the new code is the plan/subscription state machine and the window arithmetic). Entitlement is height < paidThrough, status-independent; the renewal window is [paidThrough − period, paidThrough + period) with extension always from paidThrough; renewable and expirable heights partition exactly; Cancel is terminal and refund-free with paid entitlement honored (audit Y1's latestByKey fix makes EntitledFor — the one-call integration surface — honor that across cancellation). Payments settle instantly: H == U + F, no escrow. Fee architecture is the house pattern (10% compile-time cap, plan-creation snapshot under provider consent), deployed at 0 bps.
Verification. 22 unit tests; two-round fresh-context audit (0 RED, 2 YELLOW both fixed and verified RESOLVED); Gates A and B EQUIVALENT (closure digest f440a3b3…, same dependency set as service_market); custody byte-identical (sha256 61192802…); 11 live transactions across 2 keys including two grace renewals, a permissionless expire by a non-subscriber, and a full drain to exactly 0; 15 live adversarial rejections. The fee path's success legs ran only in tests — the live run stayed at 0 bps after a harness permission denial, recorded in DEPLOYMENT.md §17 with the other untested-live cases and their covering tests.