@voltro/plugin-economy 0.0.0-stage → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +24492 -0
  2. package/LICENSE +61 -0
  3. package/README.md +2719 -2
  4. package/SECURITY.md +56 -0
  5. package/THIRD-PARTY-NOTICES.md +9841 -0
  6. package/dist/amount.js +20 -0
  7. package/dist/appleIap.server.js +262 -0
  8. package/dist/appleIapNotifications.server.js +162 -0
  9. package/dist/appleIapTrust.server.js +30 -0
  10. package/dist/bundleValue.server.js +112 -0
  11. package/dist/canonicalDataJson.js +10 -0
  12. package/dist/capture.js +53 -0
  13. package/dist/codePort.fixture.js +123 -0
  14. package/dist/codeStore.server.js +439 -0
  15. package/dist/codeTables.entity.js +43 -0
  16. package/dist/codes.d.ts +150 -0
  17. package/dist/codes.js +82 -0
  18. package/dist/components.d.ts +397 -0
  19. package/dist/components.js +283 -0
  20. package/dist/consistency.server.js +115 -0
  21. package/dist/containerStore.server.js +687 -0
  22. package/dist/containerStore.testFixtures.js +75 -0
  23. package/dist/containerTables.entity.js +45 -0
  24. package/dist/containers.d.ts +311 -0
  25. package/dist/containers.fixture.js +128 -0
  26. package/dist/containers.js +214 -0
  27. package/dist/containers.server.js +39 -0
  28. package/dist/containersPort.fixture.js +74 -0
  29. package/dist/currency.js +32 -0
  30. package/dist/declarationFixtures.js +61 -0
  31. package/dist/economy-app/events/economy.refundReversed.event.ts +1 -0
  32. package/dist/economy-app/events/economy.refunded.event.ts +1 -0
  33. package/dist/economyPort.fixture.js +124 -0
  34. package/dist/errors.d.ts +14 -0
  35. package/dist/errors.js +21 -0
  36. package/dist/events.d.ts +46 -0
  37. package/dist/events.js +1 -0
  38. package/dist/googleIap.server.js +391 -0
  39. package/dist/googleIapNotifications.server.js +130 -0
  40. package/dist/googleIapSubscription.server.js +183 -0
  41. package/dist/iap.d.ts +309 -0
  42. package/dist/iap.js +303 -0
  43. package/dist/iap.testFixtures.js +144 -0
  44. package/dist/iapAccountTables.entity.js +39 -0
  45. package/dist/iapAccounts.server.js +200 -0
  46. package/dist/iapBinding.server.js +523 -0
  47. package/dist/iapCheckout.server.js +97 -0
  48. package/dist/iapCompletion.server.js +55 -0
  49. package/dist/iapCurrentState.server.js +107 -0
  50. package/dist/iapCurrentState.testFixtures.js +22 -0
  51. package/dist/iapCurrentStatePort.fixture.js +63 -0
  52. package/dist/iapEntitlements.d.ts +79 -0
  53. package/dist/iapEntitlements.js +57 -0
  54. package/dist/iapEntitlements.server.js +48 -0
  55. package/dist/iapEntitlementsBinding.server.js +105 -0
  56. package/dist/iapEntitlementsPort.fixture.js +80 -0
  57. package/dist/iapInstalledEntitlements.testFixtures.js +75 -0
  58. package/dist/iapInstalledNotifications.testFixtures.js +93 -0
  59. package/dist/iapInventoryRefunds.server.js +261 -0
  60. package/dist/iapNotificationCustodyPort.fixture.js +67 -0
  61. package/dist/iapNotificationPort.fixture.js +73 -0
  62. package/dist/iapNotificationProcessor.server.js +134 -0
  63. package/dist/iapNotificationProcessor.testFixtures.js +63 -0
  64. package/dist/iapNotificationProcessorPort.fixture.js +55 -0
  65. package/dist/iapNotificationSchedule.server.js +69 -0
  66. package/dist/iapNotificationStore.server.js +352 -0
  67. package/dist/iapNotificationStore.testFixtures.js +31 -0
  68. package/dist/iapNotificationTables.entity.js +48 -0
  69. package/dist/iapNotificationTarget.server.js +36 -0
  70. package/dist/iapNotificationTarget.testFixtures.js +29 -0
  71. package/dist/iapNotifications.d.ts +644 -0
  72. package/dist/iapNotifications.js +6 -0
  73. package/dist/iapNotifications.server.js +95 -0
  74. package/dist/iapNotificationsBinding.server.js +232 -0
  75. package/dist/iapNotificationsSystemPort.fixture.js +50 -0
  76. package/dist/iapPlugin.testFixtures.js +59 -0
  77. package/dist/iapPort.fixture.js +74 -0
  78. package/dist/iapProviderCapture.server.js +17 -0
  79. package/dist/iapRecovery.d.ts +146 -0
  80. package/dist/iapRecovery.js +89 -0
  81. package/dist/iapRefundEventDelivery.server.js +82 -0
  82. package/dist/iapRefundEvents.js +44 -0
  83. package/dist/iapRefundEventsPort.fixture.js +59 -0
  84. package/dist/iapRefundPort.fixture.js +54 -0
  85. package/dist/iapRefundTables.entity.js +55 -0
  86. package/dist/iapRefunds.d.ts +755 -0
  87. package/dist/iapRefunds.js +6 -0
  88. package/dist/iapRefunds.server.js +553 -0
  89. package/dist/iapRefunds.testFixtures.js +64 -0
  90. package/dist/iapSpendAdmission.server.js +98 -0
  91. package/dist/iapStore.server.js +740 -0
  92. package/dist/iapStore.testFixtures.js +127 -0
  93. package/dist/iapTables.entity.js +80 -0
  94. package/dist/iapVerification.server.js +120 -0
  95. package/dist/index.d.ts +2984 -0
  96. package/dist/index.js +15 -0
  97. package/dist/intentionRecovery.js +133 -0
  98. package/dist/ledger.js +77 -0
  99. package/dist/loyalty.d.ts +274 -0
  100. package/dist/loyalty.fixture.js +92 -0
  101. package/dist/loyalty.js +311 -0
  102. package/dist/loyaltyBinding.server.js +203 -0
  103. package/dist/loyaltyPort.fixture.js +86 -0
  104. package/dist/loyaltyShopSource.server.js +90 -0
  105. package/dist/loyaltyStore.server.js +552 -0
  106. package/dist/loyaltyStore.testFixtures.js +82 -0
  107. package/dist/loyaltyTables.entity.js +49 -0
  108. package/dist/offerMerchandising.fixture.js +114 -0
  109. package/dist/offerMerchandising.js +282 -0
  110. package/dist/offerPolicies.d.ts +441 -0
  111. package/dist/offerPolicies.fixture.js +53 -0
  112. package/dist/offerPolicies.js +267 -0
  113. package/dist/piggyBanks.d.ts +272 -0
  114. package/dist/piggyBanks.fixture.js +70 -0
  115. package/dist/piggyBanks.js +200 -0
  116. package/dist/piggyBinding.server.js +204 -0
  117. package/dist/piggyPort.fixture.js +72 -0
  118. package/dist/piggyStore.server.js +610 -0
  119. package/dist/piggyStore.testFixtures.js +97 -0
  120. package/dist/piggyTables.entity.js +40 -0
  121. package/dist/plugin.js +1047 -0
  122. package/dist/port.js +12 -0
  123. package/dist/price.d.ts +99 -0
  124. package/dist/price.js +143 -0
  125. package/dist/purchaseRecovery.js +55 -0
  126. package/dist/reports.server.js +114 -0
  127. package/dist/rpc.d.ts +3201 -0
  128. package/dist/rpc.js +1200 -0
  129. package/dist/sanctionsTradeAuthority.server.js +79 -0
  130. package/dist/server.d.ts +3856 -0
  131. package/dist/server.js +43 -0
  132. package/dist/shop.d.ts +279 -0
  133. package/dist/shop.js +195 -0
  134. package/dist/shop.testFixtures.js +113 -0
  135. package/dist/shopPort.fixture.js +71 -0
  136. package/dist/shopPresentation.js +67 -0
  137. package/dist/shopStore.server.js +623 -0
  138. package/dist/shopTables.entity.js +62 -0
  139. package/dist/shopUiCopy.js +56 -0
  140. package/dist/spendLimits.d.ts +140 -0
  141. package/dist/spendLimits.fixture.js +52 -0
  142. package/dist/spendLimits.js +154 -0
  143. package/dist/spendLimitsPort.fixture.js +119 -0
  144. package/dist/subscriptionBinding.server.js +210 -0
  145. package/dist/subscriptionStore.server.js +642 -0
  146. package/dist/subscriptionStore.testFixtures.js +71 -0
  147. package/dist/subscriptionTables.entity.js +36 -0
  148. package/dist/subscriptions.d.ts +394 -0
  149. package/dist/subscriptions.fixture.js +96 -0
  150. package/dist/subscriptions.js +212 -0
  151. package/dist/subscriptionsPort.fixture.js +75 -0
  152. package/dist/systemFixtures.js +88 -0
  153. package/dist/tables.entity.js +82 -0
  154. package/dist/testing.d.ts +4615 -0
  155. package/dist/testing.js +29 -0
  156. package/dist/tradeAuthority.server.js +171 -0
  157. package/dist/tradeBinding.server.js +200 -0
  158. package/dist/tradeComponents.js +341 -0
  159. package/dist/tradeEscrow.server.js +231 -0
  160. package/dist/tradeEscrow.testFixtures.js +132 -0
  161. package/dist/tradeExpiry.server.js +42 -0
  162. package/dist/tradeRecovery.js +47 -0
  163. package/dist/tradeStore.server.js +749 -0
  164. package/dist/tradeStore.testFixtures.js +72 -0
  165. package/dist/tradeTables.entity.js +65 -0
  166. package/dist/tradeUiCopy.js +78 -0
  167. package/dist/trades.d.ts +246 -0
  168. package/dist/trades.fixture.js +102 -0
  169. package/dist/trades.js +288 -0
  170. package/dist/tradesPort.fixture.js +93 -0
  171. package/dist/walletEscrow.js +105 -0
  172. package/dist/walletEscrow.server.js +375 -0
  173. package/dist/walletKeys.server.js +4 -0
  174. package/dist/walletStore.js +570 -0
  175. package/dist/web.d.ts +2257 -0
  176. package/dist/web.js +8 -0
  177. package/package.json +161 -4
package/README.md CHANGED
@@ -1,3 +1,2720 @@
1
- # Temporary Holding Version
1
+ # @voltro/plugin-economy
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The scoped fixture uses a fresh installed account, accepted SQL period, wallet
4
+ and inventory; provider verification remains an explicit private seam. Scope
5
+ release revokes all four entitlement methods. The policy fixture calls the
6
+ actual default and narrowing path.
7
+
8
+ Partial SQL wallet and exact-price foundation. Runtime and acceptance validation
9
+ are pending. Immediate virtual/free Shop has an explicit server SQL adapter; its
10
+ optional factory binding is implemented, while IAP/ads and complete monetization
11
+ remain unfinished. No capacity claim follows from configured bounds.
12
+
13
+ ## Install and configure
14
+
15
+ `economyPlugin()` captures its currencies, service allow-list, work bounds,
16
+ clock, offer price declarations and `priceResolver`. Only this installed instance
17
+ binds to the API app's borrowed `DataStore`. It neither opens nor closes a store.
18
+ The following numbers are example configuration, not deployment pricing.
19
+
20
+ ```ts
21
+ import { defineCurrency, economyPlugin } from '@voltro/plugin-economy'
22
+
23
+ export const economy = economyPlugin({
24
+ currencies: [defineCurrency({ id: 'gold', kind: 'soft', cap: '1000' })],
25
+ realMoney: false,
26
+ offers: [{
27
+ id: 'starter',
28
+ price: { kind: 'virtual', options: [{ currency: 'gold', amount: '11' }] },
29
+ }],
30
+ })
31
+ // Add this instance to the API app's plugins array.
32
+ ```
33
+
34
+ `realMoney` defaults to `false`. Both `real` and `mixed` declarations are refused
35
+ when disabled; free and virtual prices need no store, IAP or Stripe credentials.
36
+ This switch currently applies to the implemented wallet and price foundation;
37
+ the complete virtual-only monetization stack remains unfinished.
38
+
39
+ ## Exact amounts and prices
40
+
41
+ `economyAmount()` accepts canonical decimal strings, bigint, or safe integer
42
+ numbers in the signed SQL BIGINT range. Unsafe numbers, fractions, negative zero,
43
+ leading zeroes and overflow are refused. Wire amounts and virtual prices remain
44
+ canonical decimal strings. Caps are positive exact amounts.
45
+
46
+ `definePrice(price, currencies, realMoney)` detaches and freezes `virtual`
47
+ alternatives, `real` references, `mixed` alternatives or `free`. Virtual options
48
+ name declared currencies and cannot exceed their balance cap. Real references
49
+ use positive safe integer minor units and app-owned store product identifiers;
50
+ they never verify or fulfill a payment. A mixed price uses
51
+ `{ kind: 'mixed', real: { prices, storeProductIds }, virtual: [...] }`.
52
+
53
+ `priceResolverContext()` captures a price and trusted virtual adjustments:
54
+ integer `discountBps` from zero to 10000 and an exact `minimum` per currency.
55
+ `defaultPriceResolver` is the single exported default: discounts use exact
56
+ integer arithmetic, round upward and respect the floor. `resolvePrice()` admits
57
+ custom output separately: it preserves all payment alternatives, forbids values
58
+ below the trusted floor or above the declaration and keeps real references and
59
+ store products unchanged. Personalized real prices require separately declared
60
+ store variants and verified provider prices; that integration is still open.
61
+
62
+ The installed `economy.economy.price(subject, offerId, adjustments)` facade uses
63
+ the selected resolver. It accepts only a system Subject or an explicitly allowed
64
+ verified service; a price projection grants no value. The `./pricing` subpath is
65
+ browser-safe. `./testing` supplies the actual default resolver and ledger fixtures.
66
+
67
+ ## Trusted SQL transfers
68
+
69
+ `economy.economy.service()` returns the installed `EconomyService`; request
70
+ services use the same `EconomyPort` Effect tag. `transfer(subject, input)` accepts
71
+ only system Subjects or verified services explicitly listed in `allowedServices`.
72
+ Users and guests cannot submit balances, credits, debits or transfer values.
73
+
74
+ Every transfer supplies tenant, independent `requestId`, currency, positive
75
+ exact amount, distinct source/destination accounts, a reason and a reference.
76
+ Accounts are `{ kind: 'system', id }` or
77
+ `{ kind: 'wallet', ownerKind: 'player' | 'guild', ownerId }`. At least one side is
78
+ a wallet. System-to-system and same-account transfers are refused.
79
+
80
+ The writer seeds durable admission rows, locks wallet subjects in deterministic
81
+ order, then the currency and request receipt. One transaction updates both
82
+ wallets, inserts exactly two balanced ledger postings and commits a metadata-only
83
+ receipt. The receipt has transaction ID, currency, amount and time; no owners or
84
+ references. A committed identical request returns that original receipt forever;
85
+ a changed intention with the same request conflicts. Aborted work may retain a
86
+ pending admission row but never a committed receipt or monetary posting.
87
+
88
+ Ordinary wallet balances are nonnegative and bounded by the currency cap; the explicit verified hard-currency refund path below may create policy-bound debt. Currency
89
+ configuration is persisted on first admission; later configuration drift refuses
90
+ fresh writes until an explicit migration is designed. Trusted refund debt and Trades are described below; arbitrary GM value writes remain unfinished. Optional Trades is described below. Trusted
91
+ default-wallet Escrow participants are documented below.
92
+
93
+ ## Own reads and ledger rebuild
94
+
95
+ `economy.mine` and `useWallets()` (`./web`) read only the authenticated user's or
96
+ guest's player wallets. They take no tenant, owner or value input. The query
97
+ observes wallet, subject-admission and currency tables. Missing configured wallets
98
+ project zero; guild authorization and account merging remain open.
99
+
100
+ `auditWallet()` and `rebuildWallet()` are trusted server operations. Under the
101
+ same subject/currency SQL fences they read the whole bounded wallet ledger,
102
+ validate both postings and the durable receipt state for each transaction, then
103
+ compare or repair the materialized balance. SQL remains the source of truth.
104
+ No timer, process-local mutex, native SQL client or hot cache is introduced.
105
+
106
+ `maxLedgerRows` is a work bound per wallet/currency, not a capacity promise;
107
+ `maxExportRows` bounds complete export/erase selections. Excess fails with
108
+ `limit_exceeded` rather than silently returning or deleting a prefix. A scheduled
109
+ scheduled consistency execution proof and incremental sinks/sources maintenance remain open; the bounded whole-history report is described below.
110
+
111
+ ## Privacy and inspection
112
+
113
+ The default SQL wallet declares five table roles: currencies, wallets, ledger, transactions and
114
+ subjects. Export returns the selected player's own wallets and postings. Erase
115
+ removes their wallets and owner links, redacts reference/actor fields across the
116
+ affected posting pairs, and permanently fences recreation and receipt replay.
117
+ Financial debit/credit values remain for conservation and peer rebuilds; they are
118
+ not rewritten by privacy operations. Retained deterministic fence keys are
119
+ pseudonymous, not an assertion of anonymization or a retention-policy decision.
120
+
121
+ The shared, local and Cloud panels observe `/status`: captured currency IDs,
122
+ caps, price kinds, resolver selection, SQL binding and work bounds. They do not
123
+ list wallet owners, balances, ledger values, payment references or deployments.
124
+ Unknown or invalid configuration observations fail closed; stale data is hidden
125
+ on failure. Payment installation remains false; optional Shop configuration is reported separately.
126
+
127
+ ## Prepared SQL transfer units
128
+
129
+ The explicit server adapter returned by `dataStoreEconomy` exposes
130
+ `EconomySqlService.prepareTransfer(subject, input)`. It captures trusted transfer
131
+ input, seeds pending admissions and returns an exact-store-bound opaque SQL unit.
132
+ `executeSqlMutations` (`@voltro/database/sql`) can commit it with Inventory units
133
+ and an order writer in one ordinary transaction. A later unit failure rolls back
134
+ accepted wallet values, balanced postings and receipts together. A pending row is
135
+ not payment. Ordinary `transfer` uses this same writer as a single unit.
136
+
137
+ Recreate units from original SQL identities after restart or uncertain reply.
138
+ Accepted retries preflight terminal SQL before clock. Units never authorize
139
+ player values and must not run inside another transaction. The installed
140
+ EconomyPort remains its ordinary four-method surface; prepared units belong to
141
+ the explicit server adapter. Optional Shop binding is described below; complete
142
+ spend/minor/monetization limits remain open. Source composition tests exist for runtime and all six native variants;
143
+ none has run in this build phase. See the Transactions page in the bilingual public docs.
144
+
145
+ ## Immediate virtual/free Shop SQL source
146
+
147
+ `defineShopOffer` (`./shop`) captures versioned offers with `Price`, item/currency
148
+ gifts, availability, per-player/per-day/global quantity limits and `oneTime`.
149
+ Every gift is positive and quantities are bounded before multiplication. One-time
150
+ offers have quantity/per-player limit1. Limits use the offer ID across declaration
151
+ versions; a new version does not reset one-time authority. All amounts below are
152
+ illustrative app configuration, not deployment pricing.
153
+
154
+ `dataStoreShop` (`./server`) binds explicit prepared Economy/Inventory participants
155
+ to the same exact selected app store. Item offers require Inventory; currency-only
156
+ or free offers need no Inventory or payment provider. `realMoney:false` rejects
157
+ real and mixed declarations before binding participants. With real money enabled,
158
+ this immediate writer accepts only free or selected virtual alternatives; real
159
+ payments still require the separately unfinished provider/receipt path.
160
+
161
+ ```ts
162
+ import { dataStoreShop } from '@voltro/plugin-economy/server'
163
+ import { Effect } from 'effect'
164
+
165
+ // economySql and inventorySql were constructed from this exact selected store.
166
+ const shop = dataStoreShop(store, {
167
+ currencies: [{ id: 'gold', kind: 'soft' }],
168
+ realMoney: false,
169
+ economy: economySql,
170
+ inventory: inventorySql,
171
+ offers: [{
172
+ id: 'starter', titleKey: 'shop.starter', oneTime: true,
173
+ price: { kind: 'virtual', options: [{ currency: 'gold', amount: '7' }] },
174
+ items: [{ itemId: 'potion', qty: 2 }],
175
+ }],
176
+ })
177
+
178
+ // verifiedSubject comes from the request; no owner/value is supplied by the player.
179
+ const offerView = (await Effect.runPromise(shop.offers(verifiedSubject)))[0]
180
+ if (!offerView) throw new Error('No available offer')
181
+ const receipt = await Effect.runPromise(shop.buy(verifiedSubject, {
182
+ offerId: offerView.offer.id, offerVersion: offerView.offer.version,
183
+ offerDigest: offerView.offerDigest, currency: 'gold', requestId: 'request.1',
184
+ }))
185
+ ```
186
+
187
+ The player sends only offer ID/version/server declaration digest, request, optional quantity and virtual currency.
188
+ Tenant/owner come from the verified user/guest Subject. The server captures price,
189
+ gifts and trusted eligibility. A `ShopContextSource.read` is required when an offer
190
+ names a segment, flag or countries; its implementation owns all such checks. It
191
+ can supply trusted virtual price adjustments. It runs outside SQL. Nondefault
192
+ price/context rules require an explicit version; missing prerequisites refuse.
193
+ Availability is evaluated at captured server admission time. Per-day limits use
194
+ UTC civil days; no local-time or rolling-window claim is made.
195
+
196
+ Five opt-in SQL roles are exported as `shopTables`: tenant purchase/erase/rebuild
197
+ fence, permanent subject fence, immutable offers, orders and limit counters.
198
+ The tenant fence prevents a purchase or erase from racing complete counter repair.
199
+ One shared transaction checks owner/request/definition/limits, runs the actual
200
+ prepared debit and grants, advances counters and commits the captured order and
201
+ actual child receipts. The completion timestamp is captured after preparation;
202
+ all author/source/clock callbacks remain outside replayable SQL callbacks.
203
+ Failures roll back values, counters and fulfilled order together. Pending
204
+ admissions and zero counters may remain; they are not payment or consumption.
205
+
206
+ Accepted request identity is permanent and intent-bound. Terminal retries return
207
+ the original SQL receipt before clock/source/participants; concurrent accepted
208
+ replay never recharges or grants again. Changed accepted intentions conflict.
209
+ `auditShop` / `rebuildShop` use complete bounded accepted order/definition evidence,
210
+ including retained erased global consumption. They rebuild counters only, never
211
+ repeat transfers/gifts or historical custom callbacks. Whole-history excess
212
+ refuses rather than accepting a prefix. Budgets are not measured capacity.
213
+
214
+ Export returns complete owned pending/fulfilled order intentions and receipts.
215
+ Erase clears owner links, request/price/receipt bodies and owned day/player
216
+ counters, keeps a permanent owner fence and redacted global consumption.
217
+ Shared definitions and financial conservation stay. Keys remain pseudonymous.
218
+ Shop-only erase does not undo accepted destination gifts: run the Economy and
219
+ Inventory privacy hooks as well. Complete account merge/guild lifecycle remains
220
+ open.
221
+
222
+ This explicit adapter and five-method ShopPort are implemented. The installed
223
+ economy factory/routes/inspect panel now have opt-in binding, described below;
224
+ absence of configuration still reports Shop absent. The complete virtual-only
225
+ monetization stack is not claimed.
226
+
227
+ ## Install the optional Shop
228
+
229
+ `economyPlugin({ shop: ... })` now mounts the same default SQL ShopPort or a full
230
+ `shop.service` replacement. Without `shop`, no Shop query/action/tag/table role is
231
+ mounted; `economy.economy.shop()` calls refuse unavailable. Configure both plugin
232
+ instances in one API app and import or publish actual matching item content before
233
+ buying. Example amounts remain illustrative app-owned rules:
234
+
235
+ ```ts
236
+ import { economyPlugin } from '@voltro/plugin-economy'
237
+ import { inventoryPlugin, defineItem } from '@voltro/plugin-inventory'
238
+ import { Schema } from 'effect'
239
+
240
+ const potion = defineItem({
241
+ id: 'potion', kind: 'consumable', stackable: true, maxStack: 10,
242
+ tradable: false, rarity: 'common', tags: [],
243
+ attributes: Schema.Struct({}), instanceAttributes: Schema.Struct({}),
244
+ display: { nameKey: 'items.potion' },
245
+ })
246
+ export const inventory = inventoryPlugin({ items: [potion] })
247
+ export const economy = economyPlugin({
248
+ currencies: [{ id: 'gold', kind: 'soft' }], realMoney: false,
249
+ shop: {
250
+ inventory: inventory.inventory,
251
+ offers: [{ id: 'starter', titleKey: 'shop.starter', oneTime: true,
252
+ price: { kind: 'virtual', options: [{ currency: 'gold', amount: '7' }] },
253
+ items: [{ itemId: potion.id, qty: 2 }],
254
+ }],
255
+ },
256
+ })
257
+ // Add both instances to the API app's plugins; publish/import the item catalog.
258
+ ```
259
+
260
+ The installed `economy.economy.shop()` service and ShopPort have five methods:
261
+ `offers`, `buy`, `receipt`, `exportSubject`, `eraseSubject`. `shop.offers` is a verified own
262
+ query and `shop.buy` an Action; the latter does not introduce an outer transaction
263
+ around the borrowed writer. `useShopOffers` / `useShopBuy` in `./web` use those
264
+ actual descriptors. Time-only availability needs an explicit refresh. Built-in
265
+ source-specific CDC/region/residency integration and player purchase recovery UI
266
+ remain acceptance/integration work.
267
+
268
+ The installed Inventory facade exposes `inventory.inventory.prepareGrant` and
269
+ `prepareCraft` for default SQL participation. They capture intentions and refuse
270
+ before binding, after deactivation or when Inventory/full crafting is replaced;
271
+ no SQL fallback is borrowed for a replacement. Exact-store group admission also
272
+ refuses accidental cross-app/physical-store units. Ordinary InventoryPort stays
273
+ eight methods. Prepared API, route and production discovery proof is pending.
274
+
275
+ A full `shop.service` supplies all five methods, its own durable authority/receipt/
276
+ privacy behavior and an explicit `version`. Required methods are captured without
277
+ accessor evaluation and retain their original receiver, including class methods.
278
+ Default Shop SQL roles/adapter/query sources are omitted, no fallback; actual replacement query sources must be declared. Combining a full service
279
+ with default inventory/context/price/system-account/clock/order/audit options refuses. The app
280
+ owns replacement schemas, placement/CDC, services and lifecycle; the installed
281
+ facade revokes calls after deactivation. Its privacy pair participates with the
282
+ Economy pair; Inventory hooks remain their own plugin's responsibility.
283
+
284
+ Server `/status`, strict Shared/Cloud/Local contracts and the existing bilingual
285
+ panel now show whether Shop is configured, its backend/rule/context/price slots,
286
+ redacted offer counts/limits and work budgets. Default Shop adds five roles;
287
+ replacement adds none. Rotation, built-in LiveOps and real payment remain false.
288
+ No owner, balance, order, accepted receipt, private attribute or purchase
289
+ outcome is exposed. Failure hides stale Shop content.
290
+
291
+
292
+ The default Shop inherits the root `priceResolver`; `shop.priceResolver` explicitly overrides it. An inherited or overridden custom resolver requires an explicit non-default `shop.version`, just as a custom context source does.
293
+
294
+ ## Recover an uncertain virtual purchase
295
+
296
+ The optional Shop now also mounts `shop.receipt`, a read-only live query.
297
+ `receipt(subject, originalBuyInput)` and `useShopReceipt` return the original
298
+ request ID plus `not-observed`, `pending` or `fulfilled` with the actual
299
+ receipt. Missing/pending evidence is nonterminal: it cannot prove that a
300
+ purchase will never commit and cannot justify a new identity. Reads never seed
301
+ rows, update fences, call the clock, resolve current prices, prepare a
302
+ participant or buy again. Accepted history survives current offer/policy
303
+ changes; a changed accepted intention conflicts. Erased owners are refused.
304
+
305
+ `./web` exports `createShopPurchaseRecovery` and `browserShopPurchaseJournal`.
306
+ The journal stores only the original offer/version/server declaration digest/request/currency/quantity and an opaque
307
+ app/account/tenant scope, with a version. It stores no price, balance, item state,
308
+ credentials or response body. Supply actual browser storage and Web Locks after
309
+ client initialization; neither is replaced by an in-memory fallback. The journal
310
+ checks persistence before transport and holds the shared lock through settlement.
311
+
312
+ `selectScope(scope)` invalidates old continuations every time, including a switch
313
+ away and back. Use a stable opaque scope for the authenticated app/account/tenant
314
+ and call it whenever the session selection changes. `begin(input)` durably saves
315
+ the user-selected intention; it never sends a purchase. `retry(originalInput)` explicitly
316
+ submits that saved intention through the installed buy action and settles only a
317
+ matching fulfilled receipt. `observe(result)` may settle matching SQL evidence.
318
+ Failures, malformed/mismatching evidence, unknown/pending observations and scope
319
+ changes keep the original intention. `pending()` restores it after a reload.
320
+ No automatic replay, new-ID retry, optimistic grant or manual nonterminal dismissal.
321
+
322
+ ```ts
323
+ import { browserShopPurchaseJournal, createShopPurchaseRecovery } from '@voltro/plugin-economy/web'
324
+ import type { ShopBuyInput, ShopOrderReceipt } from '@voltro/plugin-economy/shop'
325
+
326
+ export const purchaseRecovery = (
327
+ storage: Storage,
328
+ locks: LockManager,
329
+ runBuy: (input: ShopBuyInput) => Promise<ShopOrderReceipt>,
330
+ ) => createShopPurchaseRecovery(browserShopPurchaseJournal(storage, locks), runBuy)
331
+ // On the client, runBuy is useShopBuy().run without handled-error options.
332
+ // Select the authenticated scope, begin a user-selected intention, then retry
333
+ // only on explicit confirmation. Pass useShopReceipt(originalInput).data to observe.
334
+ ```
335
+
336
+ Fixed-price quote confirmation, permanent withdrawal, account-erase
337
+ browser-journal cleanup, native browser locks, complete player integration and
338
+ full provider fulfillment remain open.
339
+
340
+ ## Version-bound player Shop components
341
+
342
+ The `./components` entry now contains `ShopGrid`, `OfferCard`, `PurchaseDialog`
343
+ and `PurchaseRecovery`, using the existing shared UI primitives. They emit
344
+ intentions and render observations; they never grant items or change wallet
345
+ state optimistically.
346
+
347
+ Every offer view includes `offerDigest`, computed by the server from the complete
348
+ immutable declaration and rule version. A buy and its receipt require offer ID,
349
+ `offerVersion` and that digest. Fresh purchases compare them before preparation;
350
+ accepted replays still use immutable historical evidence. A different node with
351
+ a divergent same-version declaration cannot silently substitute another price or
352
+ gift. The browser preserves this identity in its original-intention journal.
353
+ It never computes an authorizing digest or sends a price/balance/item state.
354
+
355
+ `shopPurchaseTerms(view, intention)` shares the actual pure declaration/price
356
+ admission. It separates the currently displayed discount from the immutable
357
+ maximum charge, with exact decimal-string arithmetic. The dialog confirms that
358
+ maximum explicitly: the personalized discount is not a fixed quote. The server
359
+ may charge up to that ceiling under the saved declaration/rule identity, after
360
+ checking current eligibility/limits. A changed declaration/rule identity conflicts.
361
+ Fixed personalized-price quote confirmation and permanent withdrawal remain open.
362
+
363
+ The components accept EN/DE copy or a full `ShopUiCopy` override, translated titles,
364
+ currency names and a reward-rendering slot. Offer rows show declared gifts,
365
+ one-time policy and availability countdown using caller-supplied `at`; that clock
366
+ does not poll the backend. No remaining-stock claim follows from configured limits.
367
+ Real checkout is absent; mixed offers expose their virtual alternatives only.
368
+ The dialog uses native protected modal focus through the existing Dialog wrapper;
369
+ busy controls remain focusable and guard activation.
370
+
371
+ ```tsx
372
+ import { ShopGrid, type ShopGridProps } from '@voltro/plugin-economy/components'
373
+ import { useShopOffers } from '@voltro/plugin-economy/web'
374
+
375
+ export const PlayerOffers = (props: Pick<ShopGridProps, 'at' | 'title' | 'onSelect'>) => {
376
+ const source = useShopOffers()
377
+ return <ShopGrid source={source} at={props.at} title={props.title} onSelect={props.onSelect} />
378
+ }
379
+ ```
380
+
381
+ The parent owns the authenticated recovery scope and action admission. Obtain a
382
+ fresh request ID only for a new user-selected intention, save it with `begin`,
383
+ then call `retry` on explicit confirmation. Keep `PurchaseRecovery` above transient
384
+ offer failures/filters and use its saved request unchanged. A failed refresh hides
385
+ stale offer controls without removing saved purchase recovery. Closing the dialog
386
+ does not delete a request. `useShopReceipt(undefined)` skips the query; mount it
387
+ with the original saved input and pass matching data to `observe`. Only actual
388
+ fulfilled evidence settles it. Account-erase browser-journal cleanup remains open.
389
+
390
+ DOM fixtures substitute the modal methods and lock seam; they do not establish
391
+ native focus trapping, Web Locks, browser layout, dialect parity or a complete
392
+ game flow.
393
+
394
+ The explicit retry takes the exact original input shown to the user:
395
+ `retry(originalInput)`. It captures it before waiting for the shared lock and
396
+ compares it with the current durable reference before transport. If another tab
397
+ has settled the old request and reserved a new valid purchase, the stale control
398
+ conflicts rather than silently sending that new purchase. Pass the request
399
+ received by `PurchaseRecovery.onRetry`, not a newly minted input.
400
+
401
+ ## Replace the complete wallet port
402
+
403
+ `EconomyPort` keeps four methods: `transfer`, `mine`, `auditWallet`,
404
+ `rebuildWallet`. `EconomySystemService` adds the required `exportSubject` and
405
+ `eraseSubject` pair for a full installed replacement. Supply that service
406
+ through `economyPlugin({ currencies, service, version, querySources })`. An
407
+ explicit non-default version and a nonempty, unique list of the actual app-owned
408
+ SQL tables for wallet query invalidation are required. The installed
409
+ `economy.mine` route uses those sources; it does not keep the default SQL
410
+ sources. Required methods are captured once without evaluating accessors; class
411
+ receivers are preserved through the same admission used by Shop replacements.
412
+
413
+ The factory omits all five default wallet tables and never constructs or falls
414
+ back to its default SQL adapter. `now`, `maxLedgerRows` and `maxExportRows` are
415
+ refused with a replacement. The app owns its SQL authority, declared schema,
416
+ conservation, caps, durable request receipts, complete audit/rebuild, lifecycle,
417
+ placement/CDC and privacy behavior. Normal app binding activates the facade;
418
+ deactivation revokes it. A replacement does not turn an unbound facade into a
419
+ working service. The four-method Effect tag and authenticated own-wallet route
420
+ keep the same output contract.
421
+
422
+ The installed facade still refuses player-authored transfer/audit/rebuild values,
423
+ undeclared transfer currencies and invalid own-subject identity before delegation.
424
+ Only system Subjects or verified services in `allowedServices` can transfer or
425
+ repair. Root price projection remains independent of the wallet backend. The
426
+ replacement must enforce its remaining state and tenant invariants; none is
427
+ proved by the presence of a method. Directly providing a different EconomyPort
428
+ Layer outside this plugin makes the app responsible for equivalent admission.
429
+
430
+ A default SQL Shop is refused over a fully replaced wallet: it cannot silently
431
+ charge a different default ledger. Supply a complete Shop replacement too, or
432
+ leave Shop absent. Each full Shop requires explicit `shop.querySources.offers`
433
+ and `shop.querySources.receipt`, nonempty unique actual app-owned SQL table lists,
434
+ and a non-default rule version. Default Shop refuses replacement query sources;
435
+ full Shop refuses default clock/participant/context/price/system-account and
436
+ order/audit bounds. The factory retains neither default Shop roles nor query
437
+ sources for that backend. Its dependent five-method ShopPort stays unchanged.
438
+
439
+ All four inspect layers now distinguish SQL/custom wallet backend and rule
440
+ version, normal app binding and conditional table roles. Default SQL work bounds
441
+ are `null` for a replaced wallet or Shop; panels explicitly label them
442
+ app-owned. No owners, wallet values, purchase outcomes or private replacement
443
+ table names are exposed. Failure hides stale configuration.
444
+
445
+ ## EconomyPort fixture Layer
446
+
447
+ `./testing` exports `economyPortFixture({ currencies, allowedServices?, now?
448
+ })`: a test-only in-memory four-method port plus its actual `EconomyPort` Layer.
449
+ It reuses `planLedgerTransfer` and `applyWalletPosting` for exact balanced
450
+ rules, stable request receipts, own reads and ledger-derived audits. Provide
451
+ `fixture.layer` with `Effect.provide` when testing a dependent system. This
452
+ fixture grants no SQL, native, durable restart or production proof, and is not a
453
+ deployment backend. The complete system-contract conformance remains open.
454
+
455
+ The installed default `economy.economy.prepareTransfer` facade participates in same-store atomic game-currency Inventory crafting, refusing inactive/full-replacement SQL borrowing. Ordinary EconomyPort stays four methods.
456
+
457
+ ## SQL sources and sinks aggregate
458
+
459
+ `readEconomyFlows(store, options)` and `economyFlowsAggregate(options)` live
460
+ only in `./server`, for the default SQL Economy tables. A replaced wallet owns
461
+ its report; these helpers never switch to its sources or a hot cache. Install
462
+ the factory explicitly as an app-owned `*.aggregate.ts` default export. The
463
+ existing aggregate discovery/storage/ refresh/inspection lifecycle owns its
464
+ materialization. No new always-on plugin source, timer or boot change. The API
465
+ Economy template now authors this file. The example tenant and bounds below are
466
+ illustrative configuration budgets.
467
+
468
+ ```ts
469
+ // aggregates/economyFlows.aggregate.ts — server-only file convention
470
+ import { economyFlowsAggregate } from '@voltro/plugin-economy/server'
471
+
472
+ export default economyFlowsAggregate({
473
+ tenantId: 'game-demo', name: 'economy.flows', refresh: '1h',
474
+ maxRows: 10000, maxGroups: 1000,
475
+ })
476
+ ```
477
+
478
+ The reader captures tenant/bounds before laziness and takes a bounded whole
479
+ ledger observation through the borrowed store transaction. It uses the actual
480
+ wallet-audit evidence reader, including durable committed/erased transaction
481
+ receipts, exact conserved pairs, account/identity and matching reason/reference/
482
+ actor checks. Missing pairs/receipts, corrupt values or exceeded work/group bounds
483
+ refuse the entire refresh. Pending receipts carry no value and are not counted.
484
+ Financial rows retained after subject erasure remain in the flow totals; no
485
+ owner, reference, actor, receipt body, wallet balance or tenant ID is output.
486
+
487
+ One accepted transfer counts once per UTC day/currency/reason: system→wallet is
488
+ a source, wallet→system a sink, wallet→wallet a separate transfer total. Both
489
+ sides are never double-counted. All sums use BigInt and canonical nonnegative
490
+ strings, so a report total may exceed one wallet's signed BIGINT while remaining
491
+ exact. Default bounds are 10,000 ledger rows/1,000 groups; configurable maxima
492
+ 100,000/10,000 are admission budgets, not measured capacity. Full history is
493
+ recomputed; incremental retention/archive/large-history reporting is unfinished.
494
+ The projection does not promise a serializable point-in-time snapshot across
495
+ concurrent writes, or refresh a player's wallet value.
496
+
497
+ Normal aggregate replacement preserves framework stale/error metadata; authorize
498
+ all app query/GM/report reads using the actual aggregate APIs and tenant policy.
499
+ Direct server helpers have no player route.
500
+
501
+ ## Bounded SQL consistency schedule
502
+
503
+ The default installed facade exposes
504
+ `economy.economy.checkConsistency(systemSubject, input)`, and `./server` exports
505
+ `economyConsistencySchedule` for an explicit app-owned `*.cron.tsx` file. The
506
+ API Economy template now authors that schedule. Existing scheduler admission,
507
+ coordination, overlap, watchdog and run/error tracking own execution; no private
508
+ timer, workflow queue, scheduler replacement or default boot source is added.
509
+
510
+ ```ts
511
+ // schedules/economy.checkConsistency.cron.tsx
512
+ import { economyConsistencySchedule } from '@voltro/plugin-economy/server'
513
+ import { installed } from '../system/installed.server'
514
+
515
+ export default economyConsistencySchedule({
516
+ tenantId: 'game-demo', runner: installed.economy,
517
+ cron: '0 2 * * *', timezone: 'UTC',
518
+ maxLedgerRows: 10000, maxWallets: 1000,
519
+ })
520
+ ```
521
+
522
+ Whole bounded SQL ledger evidence is admitted by the same actual conserved-pair
523
+ reader used by wallet audit/report. The union of live financial ownership and
524
+ actual wallet materializations includes a wallet even if its materialization was
525
+ deleted. Canonical materialized identities and known currencies must match.
526
+ Each selected wallet uses the existing actual audit under its ordinary durable
527
+ fences. Audit may seed/fence metadata; this job never invokes repair or changes
528
+ balances. It returns only checked/mismatch/ledger-row counts and a matches flag.
529
+ Erased ownership is excluded; retained anonymous financial evidence is still
530
+ validated. Foreign tenants are not selected. System Subjects only; player calls
531
+ refuse before SQL. Lazy input capture, inactive/replaced root refusal and no
532
+ fallback preserve the installed owner. A replacement owns its consistency job.
533
+
534
+ A mismatch fails the ordinary cron body with a redacted conflict code. Corrupt,
535
+ erased-during-run, unknown-currency, missing or oversized evidence makes the job
536
+ fail rather than report a prefix as green. Default whole bounds are 10,000 ledger
537
+ rows and 1,000 wallet/currency units; configurable maxima 100,000/10,000 remain
538
+ budgets. A transfer after enumeration belongs to the next run; this is not a
539
+ serializable fleet-wide cut. Operator-controlled rebuild remains the existing
540
+ separate trusted operation. No automatic compensation or missed-workflow crash
541
+ recovery is claimed; recurring scheduler behavior still needs actual execution.
542
+
543
+ No run number or support claim follows.
544
+
545
+ ## Canonical system-contract fixture exports
546
+
547
+ `./testing` exports `priceResolverSystemFixture`, `economyFakeLayer` and
548
+ `economyPortSystemFixture` beside the existing declaration and configurable
549
+ four-method fixture. The slot probe uses the actual exported Price default and
550
+ an admitted composed override with a distinct result. The canonical fake Layer
551
+ creates one fresh memory fixture per Effect scope; its release closes that
552
+ fixture and escaped service calls refuse. Explicit `economyPortFixture(options)`
553
+ also exposes idempotent `close`; its ordinary Layer.succeed remains owned by the
554
+ caller's fixture lifetime. It calls shared pure ledger planning/wallet
555
+ arithmetic, no SQL imitation or production backend.
556
+
557
+ These exports fit the existing system suite's fixture shape; they do not
558
+ establish per-table privacy/event/inspect/mixin/upgrades or a complete
559
+ `systemContractConformance` registration/pass. SQL durability/native evidence
560
+ comes from the real adapter scenarios, not this memory Layer.
561
+
562
+ ## Starter packs and explicit offer experiments
563
+
564
+ The browser-safe `./offer-policies` entry provides `defineStarterPack`, `starterPackAvailable`, `defineOfferExperiment`, `defaultExperimentBucket`, `resolveOfferExperiment` and `offerPolicyContextSource`. These functions compose the existing Shop declaration, trusted context and SQL writer. No new plugin, store, hot cache, parallel grant path or always-on boot work.
565
+
566
+ A Starter pack captures one existing offer with quantity1/per-player1/oneTime, an explicit positive first-session duration and optional exact currency multiplier. The multiplier becomes immutable offer content and is validated against the actual currency cap. Existing unversioned Shop counters accept the pack once per owner, including alternate request IDs; an accepted original request replays before clock/context even after the offer expires. The availability window is half-open from the trusted first session. This is per-offer first-purchase bonus, not an atomic account-wide never-paid rule. Account-merge identity continuity is still open.
567
+
568
+ Experiments capture explicit ID/version, half-open UTC dates, sorted weighted variants, complete distinct immutable Shop offers and metric names. Default bucketing is deterministic from tenant/experiment/version/trusted canonical key; it is no security RNG or durable assignment. The identity source must preserve this key across guest upgrade/merge. Real/mixed variants require distinct store products per configured channel. `realMoney:false` refuses real/mixed variants at declaration; virtual/free variants require no third-party credentials. Each variant keeps its own product/offer/digest, never a client-forged storefront price or a replacement real-price reference. Exposure events, persisted assignment/metrics/GM experiment reports and provider settlement remain open.
569
+
570
+ `offerPolicyContextSource` binds the real class/function receiver, captures configuration and reads trusted facts outside SQL callbacks. Its source receives only verified tenant/owner scalars, admitted offer and observation time; auth scopes/provider metadata are not forwarded. It combines existing eligibility and floor-validated virtual adjustments with Starter windows and the selected experiment offer. Altered declared content refuses. Add the actual resulting offers and this context source to the ordinary Shop configuration with an explicit nondefault rule version. The application still owns authoritative first-session/key/segment/flag/country facts. Snapshot eligibility is no serializable mutable-profile/spend-limit contract. Missing source data refuses; no dummy facts or fallback.
571
+
572
+ Existing inspect surfaces show the configured custom context; dedicated experiment/Starter controls remain open.
573
+
574
+ ## Exact container disclosure, pity and server sampling
575
+
576
+ The browser-safe `./containers` entry captures `defineContainer` content with explicit currency registry/global money switch, immutable ID/version/shared-counter group, ordinary Price, bounded positive weighted drops and optional pity. Gifts use actual currency caps and exact signed-BIGINT amounts. `pity.guaranteed` names an existing rarity; `after` is the guaranteed attempt count, and optional `softFrom`/`softRampBps` use exact integer basis points. No float accumulation. Example counts and probabilities are app-owned test configuration.
577
+
578
+ `containerProbabilityContext` captures the real currency registry and declaration before evaluation. `defaultContainerProbability` preserves relative weight within guaranteed/non-guaranteed groups while increasing the guaranteed probability by an explicit basis-point increment from the soft threshold. `resolveContainerDistribution` admits detached nonnegative integer weights, all known unique drop IDs and a positive bounded total; custom slots cannot violate hard pity. `discloseContainer` exposes the same exact numerator/denominator values used by `resolveContainerRoll`. Hard pity permits only the guaranteed rarity on attempt `after`; a hit resets misses to zero. Without pity there is no miss counter. Public rational disclosure is exact and may exceed wallet BIGINT; it is not a floating percentage.
579
+
580
+ `defaultContainerRoll` is the single weighted selection default. `ContainerRandomPort` is explicit server authority; the `./server` exports `cryptoContainerRandom`, `cryptoContainerRandomLayer` and `sampleContainer`. The native source uses cryptographic rejection sampling, masks excess bits and refuses after a budget of128 rejected draws. No modulo bias, Math.random, player seed/state or automatic backend switch. Sample preparation captures caller data, evaluates the probability slot once and reuses that exact distribution after deferred draw. Custom roll must choose an admitted positive-weight outcome; custom random ports must supply uniform bounded draws.
581
+
582
+ This is **sampling, not opening/fulfillment**. No player container route, debit, grant, persistent group counter, SQL journal/rebuild, privacy role, age/country gate or atomic payment/pity acceptance is installed. Ordinary authoritative acceptance must commit the chosen result with actual prepared debit/grants and owned durable history; retry must never draw again. Dedicated four-layer container controls and the full legal/virtual-only contract remain open. Test-only random Layer and actual default/override/port probes are in `./testing`.
583
+
584
+ `offerExperimentFixture` and `experimentBucketSystemFixture` expose the actual deterministic default and a distinct admitted override through the server/testing entry.
585
+
586
+ ## Code redemption adapter
587
+
588
+ `./codes` declares `defineCode`, CodePort and code intentions. A declaration contains immutable ID/version/campaign, actual capped currency/item gifts, half-open dates and explicit positive global/per-player limits. It contains no code token. Players supply only case-sensitive code and original request ID; amounts, recipients, gifts and state remain server-owned. Runtime code values are provided privately, never examples, credentials or placeholder tokens.
589
+
590
+ The server `dataStoreCodes` borrows one ordinary DataStore and the actual Economy/Inventory prepared participants. Four exported roles are `_voltro_eco_code_tenants`, `_voltro_eco_codes`, `_voltro_eco_code_subjects`, `_voltro_eco_code_claims`. SQL stores only scoped SHA-256 code digests and captured declarations, never plaintext codes. Issuing is trusted system/explicit service only. Definition/code identity is immutable; a changed declaration under the same code refuses.
591
+
592
+ Tenant/owner fences serialize acceptance and erasure. Bounded complete accepted claim history is the global/per-owner counter truth; no independent mutable counter cache. Actual wallet postings, item movements and code receipt commit through the same exact-store prepared group. Rejected grants roll back wallet value and leave a pending original identity, never accepted gifts. Captured completion time and code windows are checked before acceptance preparation. Terminal original retry returns before current clock/declaration/provider preparation. Accepted intent retains its original currency registry for replay/privacy after declaration removal. Own receipt reads distinguish pending/not-observed from fulfilled and never authorize a fresh nonce.
593
+
594
+ Export/erase methods are complete for these roles. Subject erasure clears owner/intent/digest/receipt bodies, preserves a permanent subject fence and accepted flag/code-key/owner-key evidence so global usage cannot reset. Shared definitions remain. Retained deterministic keys/digests are pseudonymous, not anonymous. Owner export after erasure is empty. Cross-system wallet/item privacy still runs through those owning plugins; code-only erasure cannot revoke gifts. Financial/movement child-reference conformance and archival retention require further evidence.
595
+
596
+ Optional `economyPlugin({ currencies, codes: { inventory } })` now installs CodePort, four default SQL roles, `economy.redeem` action and owned `economy.codes.receipt` live query. Omit inventory for currency-only gifts; item declarations without a participant refuse. No Code config means no Code tables, routes or Layer. `economy.economy.codes()` exposes the captured installed service. Trusted `create` accepts a privately supplied runtime code; no player or inspect issuance action is mounted. `useRedeemCode`/`useCodeReceipt` in `./web` use the actual browser-safe descriptors. The client sends only code/request identity; a missing or pending observation never authorizes a new request. Durable client recovery and account-erase client cleanup remain open; do not store code values in a browser journal or logs.
597
+
598
+ `defaultCodeEligibility` is the single exported default. `resolveCodeEligibility` captures the declaration, timestamp and accepted SQL counts; custom pure `codes.eligibility` may narrow admission, never raise hard dates/limits. Custom rules require explicit non-default `codes.version`. Callbacks run outside replayable SQL callbacks; acceptance rechecks captured count basis for custom rules and hard limits under fences. Persisted definitions bind rule versions; accepted receipts preserve historical rule identity and currencies without comparing current configuration on terminal retry. No cross-process/native race claim is inferred from these source checks.
599
+
600
+ A full `codes.service` replacement must implement all five CodeService methods, use non-default `codes.version` and supply nonempty actual `codes.querySources.receipt` tables. Required methods are captured with class receivers and without evaluating accessors. Replacement owns schema/CDC/authority/receipts/complete privacy; no default roles, bounds or fallback. Default participant/clock/policy/bound options are refused with a replacement. Default SQL Codes over a fully replaced wallet refuses. Installed creation retains trusted Subject checks, player routes derive own verified Subject, and deactivate revokes delegation. Normal export/erase invokes root Economy plus each configured optional system; destination Inventory privacy remains owned by Inventory.
601
+
602
+ Server `/status`, strict Shared/Cloud/Local contracts and the bilingual panel now show installed backend, rule/eligibility binding, item participant and work bound, never token, subject or accepted outcomes. Default Codes adds four roles; full replacement adds none and reports null SQL bounds. Referral and durable client recovery remain explicitly absent. Existing read-only providers carry the same observation; no new write capability is implied. `./testing` provides `codeEligibilityFixture`, the actual default/override probe, fresh scoped `codesFakeLayer` and actual installed CodePort probe. Production discovery/auth/live delivery, generic conformance, child-reference/archival proofs, GM issuance, referral workflow and complete monetization remain open.
603
+
604
+
605
+ ## Durable SQL Container opening
606
+
607
+ `./server` now exports `dataStoreContainers`, captured `resolveContainersStoreOptions`, `containerDefinitionDigest` and four separate roles: container tenants, immutable definitions, subject fences and opening history. `ContainersPort`/five-method `ContainersService` lives in browser-safe `./containers`. The adapter borrows actual prepared Economy/Inventory writers; no alternate debit/grant path, hot data store or tick work.
608
+
609
+ Configured content uses real Price, optional allowed-country content and shared Pity groups with identical policies. `realMoney:false` refuses real/mixed declarations; free/virtual opens need no providers. Real-payment fulfillment remains absent. Required `accessSource` supplies trusted age/country/eligibility/virtual adjustments for the verified owner. No unknown-age/adult fallback. `defaultContainerEligibility` and `resolveContainerEligibility` preserve no-minors/country/eligibility guards; custom pure eligibility may narrow only. App source and rules require explicit non-default version. Actual age-verification/provider integration and approved legal exemptions remain open.
610
+
611
+ Own list reports exact Pity-adjusted probabilities, immutable declaration/rule identity and a server `disclosureDigest` tied to owner/history/distribution. An opening intention contains those observed identities, request ID and optional virtual currency, never a draw, seed, amount, probability or player state. Fresh opening conflicts if SQL history or same-version probability output no longer matches the observed disclosure. Current displayed discount is not a pinned quote; the captured declaration bounds payment. Source/RNG/custom policy callbacks occur outside replayable SQL callbacks.
612
+
613
+ One unresolved original request per owner serializes openings across groups. SQL reserves declaration, original currency registry, historical sequence/Pity and exact disclosed distribution before sampling. The admitted draw/drop/price is durably selected before actual participants prepare. Repeated pending requests reuse that selection; concurrent initial samplers may draw before one SQL selection wins, but only the admitted selection can fulfill. No guarantee that RNG is invoked exactly once across a pre-selection crash. A new nonce cannot bypass a pending original. Failed grants roll back wallet/items/accepted sequence and retain the original selection; fixing actual stock allows the same request to proceed. Permanent cancellation/GM repair of unresolved originals remains open; do not substitute a new request.
614
+
615
+ Prelude rechecks owner/pending/selected content/history/Pity under fences. Actual debit, chosen currency/item grants, accepted opening receipt and owner-slot release share one exact-store SQL group. Sequence-ordered accepted history is Pity truth/rebuild source; there is no mutable Pity counter/cache to reconstruct. Shared groups require matching historical policy. Hard Pity resets on a hit; fresh disclosure changes after acceptance.
616
+
617
+ Terminal original retry/own read admits historical content, rules, distribution and child receipts before current declarations/clock/facts/RNG/preparation. Export/erase owns these four roles after declarations change; erase removes subject/intention/selection/receipt bodies and leaves permanent owner fences/pseudonymous IDs. Shared definitions remain. Wallet/item erasure still runs through those owners. Factory/replacement/lifecycle are now available below.
618
+
619
+ Facts/RNG are explicit fixture seams, not age-verification or entropy proof. Native DDL/cleanup includes all four roles. `containerEligibilitySystemFixture` uses the actual default/override; canonical installed ContainersPort Layer is now available below; full registration remains open.
620
+
621
+
622
+ ## Installed optional Containers
623
+
624
+ `economyPlugin({ currencies, containers: { containers, version, accessSource, inventory } })` now installs the captured SQL ContainersPort, four optional roles, own `economy.containers`/`economy.containers.receipt` queries and `economy.openContainer` action. Omit inventory only for currency-only drops. Required trusted access source supplies actual age/country facts with no adult default; it requires explicit non-default rule version. The root `realMoney` switch controls every declaration. Root price resolver is inherited unless explicitly overridden. Only configured systems add roles/routes/Layers; `economy.economy.containers()` refuses while absent or inactive.
625
+
626
+ Default receipt/list sources are actual Container SQL tables. Optional `containers.querySources` adds real app-owned invalidation tables for external access/price facts; clocks and non-SQL facts still require explicit freshness integration. A complete five-method `containers.service` replacement requires non-default version and actual nonempty list/receipt source tables. It owns schema/CDC/legal authority/receipts/privacy with no default SQL roles/bounds/fallback. Default participant/clock/strategies/random/source/bound options are refused with a replacement. Default SQL Containers over a completely replaced wallet refuses. Required methods retain class receivers without reading getters. Installed gateway preserves own verified Subject checks; replacement remains responsible for its legal/financial contract.
627
+
628
+ Normal bind/deactivate/export/erase invokes each configured optional system. Economy and Container hooks own their roles, while Inventory still owns destination erasure. Backend/rule identity, probability/roll/eligibility/price/RNG binding, redacted content counts/groups/Pity thresholds and work bounds are shown by server status, strict Shared/Cloud/Local decoder and bilingual configuration panel. No player Pity, draws, owner data, selections, outcomes or wallet values. Custom service reports app-owned RNG/null SQL bounds; real-payment/GM/client-recovery installation remains false. The existing read-only provider paths carry the new observation; refresh errors hide stale content and retain the reload control/focus. No GM write capability is implied.
629
+
630
+ `useContainers`, `useOpenContainer` and `useContainerReceipt` use the actual browser-safe descriptors. They neither persist a client journal nor replay automatically. Send only the observed declaration/disclosure identities and original request/currency choice. Missing/pending stays nonterminal. Uncertain selected originals cannot be discarded for a new nonce. Full player components, native/browser durable recovery, original cancellation/GM repair and real-payment fulfillment remain open.
631
+
632
+ `./testing` now exports actual installed scoped `containersPortFixture`, `containersFakeLayer`, `containersPortSystemFixture` and the actual default eligibility probe. Each scope uses a fresh memory store and revokes escaped service access through normal plugin deactivation. Explicit test facts/RNG are seams, not production age or entropy proofs. Templates await actual trusted age-source integration; no fake adult default is installed there.
633
+
634
+
635
+ ## Subscription calendars and daily-grant rules
636
+
637
+ The browser-safe `./subscriptions` entry provides `defineSubscription`, `SubscriptionsPort`, replacement-method capture and intention admission. Content includes the existing Price, a day/week/month period, captured on-purchase/daily item and currency gifts, grace days and required duration/renewal copy keys plus an app-local cancellation path. The global `realMoney:false` switch refuses real/mixed declarations; virtual/free content requires no provider credentials. Cooling-off defaults to fourteen days. A shorter declaration requires an explicit application reason; this config is not a legal-compliance result. Perks and country/age authority remain unfinished.
638
+
639
+ `defaultSubscriptionPeriod` uses UTC, exact day/week durations and calendar months anchored to the original purchase time. A Jan31 origin clamps to Feb28/29 and returns to Mar31; it never derives the next month from the previously clamped date. `resolveSubscriptionPeriod` captures content and time before the slot. A custom resolver requires an explicit non-default rule identity and may narrow the canonical half-open window, never extend or overlap it. It may leave a gap. Work counts/time ranges are admission budgets, not capacity claims.
640
+
641
+ `defaultSubscriptionDailyGrant` decides eligibility in active or explicitly admitted grace windows. `resolveSubscriptionDailyGrant` uses an absolute UTC day, rejects duplicate claimed days and requires a named custom rule. A custom policy can deny an eligible grant but cannot permit a duplicate, early, expired or out-of-grace claim. The caller must supply complete authoritative SQL receipt history; this pure decision does not persist or prove idempotency. Local client timezone and client-supplied state are never grant authority.
642
+
643
+ `SubscriptionsPort` declares list/active/start/renew/claimDaily/cancel/export/erase. A complete replacement retains class receivers and refuses method getters. Player start intents carry observed content/version/digest, original request and currency choice. Daily/cancel intents carry only entitlement/request IDs. Renew is a trusted server intention; implementations must deny players and independently fence the entitlement/period. No price, dates, consent, claimed days or provider state is accepted as player value authority. Optional factory installation is described below.
644
+
645
+ No new tables, settlement, automatic debit, failure-grace transition, Inbox delivery, privacy implementation, SQL rebuild, panel installation, provider integration or canonical port fake Layer is implied. No support claim follows from these declarations.
646
+
647
+
648
+ ## Ordinary virtual Subscription SQL adapter
649
+
650
+ `./server` now exports `dataStoreSubscriptions`, option admission, declaration digest and four Subscription table roles. Optional factory installation of this borrowed-DataStore adapter is described below. Explicit trusted access/renewal-consent source and non-default rules identity are mandatory; there is no consent fallback. The source receives verified owner/tenant scalars, captured content, start/renew kind and observation time. It runs outside replayable SQL callbacks. Real/provider settlement is absent; the adapter can select free or virtual alternatives through the actual Price guard.
651
+
652
+ Start uses observed declaration/version/digest, original nonce and currency choice. Actual prepared wallet debit/on-purchase currency gifts/item grants, accepted event and entitlement commit in one exact-store group. A genuine item/funds failure accepts no entitlement or value; participant pending admissions may remain. Complete sequence-ordered accepted events reconstruct current entitlements and UTC claimed days; there is no mutable entitlement/day cache. One existing unexpired/grace entitlement per content ID refuses another start. A new lifetime after expiry has its own origin and entitlement identity. Bounded complete history is required; archive/retention extension is open.
653
+
654
+ Daily claims carry only own entitlement/request IDs. Original accepted requests return historical receipts before clock/current declaration/source/preparation; another nonce cannot grant the same entitlement/UTC day. SQL prelude serializes owner history and checks the captured sequence basis. Parallel changed bases conflict and must reread; the framework does not silently retry callbacks or invent a new nonce. Preparation completion rechecks expiry and UTC-day crossings before acceptance. Child receipts use actual existing wallet/item writers, not duplicated transfer/grant code.
655
+
656
+ Renew is system-only and takes explicit tenant/owner/entitlement/request IDs. It requires original renewal consent plus current trusted consent/eligibility, due expiry and the next anchored window. It accepts at most one next period independently of nonce. It neither charges future periods early nor bulk-charges old missed months. Scheduling, durable consent acquisition/revocation events, payment-failure transitions and overdue repair remain open. Configured grace permits daily grants after expiry; cancellation suppresses that grace. This is not a verified provider-payment-failure policy. Cancel removes renewal authority without shortening an already paid active period or writing a refund; withdrawal/refund settlement is open.
657
+
658
+ Owning export/erase covers all four roles after current content/currencies change. Erase clears owner/event intent/receipt bodies and retains permanent subject fences plus pseudonymous event IDs/sequence/time; shared immutable declarations remain. Wallet and Inventory retain their separate privacy responsibilities. No Tick, hot cache, native connection or always-on boot work is introduced by this opt-in server factory.
659
+
660
+ Native DDL/cleanup includes each new role. Read/reconstruction, conservation, genuine grant rollback, UTC nonces, original replay, trusted renewal, class consent receiver, cancellation and owning privacy are source scenarios, not observed runtime results.
661
+
662
+
663
+ ## Installed optional Subscriptions
664
+
665
+ `economyPlugin({ currencies, subscriptions: { subscriptions, version, accessSource, inventory } })` now installs the default SQL `SubscriptionsPort`, its four roles, normal lifecycle/privacy and own list/active/start/daily/cancel RPCs. Omit Inventory only when both grant declarations contain no items. Root `realMoney` and Price resolver are inherited; no source or consent is manufactured. No configuration means no Subscription roles/routes/Layer; absent or inactive facade access refuses. Trusted `renew` stays server-only, with no player renewal RPC or built-in scheduler. Earlier statements that this factory is absent are superseded.
666
+
667
+ A complete nine-method `subscriptions.service` replacement requires explicit non-default version, content and actual nonempty list/active query sources. It retains class receivers without invoking getters, owns authoritative schema/CDC/consent/receipts/lifecycle/privacy and has no default SQL roles, participant options, work bounds or fallback. Default SQL Subscriptions over a completely replaced wallet refuses. Default query sources can add actual app source tables; external fact and clock freshness remain explicit application integration. No native connection or Rooms/CLI boot hook is added.
668
+
669
+ Server status, strict Shared/Cloud/Local observations and EN/DE panel show backend/rule identity, period/daily/price slots, declaration IDs/periods/grant type counts/grace and SQL work bounds or app ownership. They expose no consent documents, owner IDs, active entitlements, receipts or values. Real-payment/automatic-renewal/GM/client-recovery capabilities remain false. Existing read-only selected-mount/replica providers carry the full observation; failure hides stale configuration and preserves the reload control/focus. No GM write permission follows from this panel.
670
+
671
+ `useSubscriptions`, `useActiveSubscriptions`, `useStartSubscription`, `useClaimSubscriptionDaily` and `useCancelSubscription` use the actual browser descriptors. They neither persist original intents nor automatically replay or schedule renewal. List displays declaration ceilings, not a pinned personalized quote. Applications must render required duration/renewal/cancellation disclosure and implement consent acquisition before supplying authority. Built-in player components, read-only original-intent recovery query/journal, Inbox/perks, consent/revocation events, failure/overdue repair and provider/refund/withdrawal remain open. Templates do not install fake consent.
672
+
673
+ `./testing` exports actual installed fresh memory-store `subscriptionsPortFixture`, scoped `subscriptionsFakeLayer` and the actual port/default slot probes. Normal plugin deactivation revokes escaped services. Consent facts are explicit test seams, not production/legal evidence.
674
+
675
+
676
+ ## Read-only Subscription originals
677
+
678
+ `SubscriptionsPort` now has nine required methods, adding `receipt`; earlier eight-method references are superseded. `economy.subscriptions.receipt` and `useSubscriptionReceipt` observe a captured original start/daily/renew/cancel intention for the verified owner only. A start read carries its original declaration/version/digest/currency. Other reads carry entitlement/request IDs. Trusted renewal observation derives owner/tenant from the player's verified Subject; it does not grant renewal authority or mount a renewal action.
679
+
680
+ Default SQL observation performs reads only. It seeds no rows, evaluates no clock/current declaration/consent/strategy/prepared writer and neither reissues nor cancels an uncertain operation. Accepted originals use retained immutable definition/receipt evidence; changed accepted intents conflict. Missing evidence is `not-observed`, a nonterminal observation that cannot authorize a replacement nonce. Erased owners refuse. There is no pending event value row before the atomic group commits, so absence may coexist with an in-flight group. Full replacements must implement this ninth method and declare actual nonempty `querySources.receipt` tables alongside list/active.
681
+
682
+ Tests inspect all owned rows before/after the actual read and put throwing clock/authority/preparation controls behind it. Installed route/replacement/schema sources are aligned.
683
+
684
+
685
+ ## Loyalty declarations and rules
686
+
687
+ The browser-safe `./loyalty` entry provides `defineLoyalty`, exact purchase-point projection, tier/perks strategies and the eight-method `LoyaltyPort`. Content captures ID/version/title, named real or virtual purchase sources with exact positive integer numerator/denominator, unique ordered tiers beginning at zero and XP multiplier/discount/badge/Inbox-gift perks. Gift admission reuses the actual Shop/currency caps. Virtual sources use the app currency registry; real sources require the global money switch and an ISO-style currency code. `realMoney:false` refuses real sources before binding and needs no provider credentials for virtual points. All limits are configured admission budgets, not measured capacity.
688
+
689
+ `loyaltyPointsForPurchase` projects trusted accepted amounts with exact BigInt arithmetic and floors once per purchase. It neither verifies a provider receipt nor writes points. `defaultLoyaltyTier` selects the highest earned threshold; named overrides may choose only an earned declared tier. `defaultLoyaltyPerks` returns its declared perks; the named perks slot may narrow only to a unique subset of those exact perks. Hidden, amplified or unearned perks refuse. These pure rules apply no Config override, XP/Price hook, badge or Inbox gift.
690
+
691
+ `projectLoyaltyPoints` is the one whole accepted-history arithmetic/graph projection used by the SQL writer and rebuild. It captures original purchase amount/weights and cumulative linked refunds; sequence gaps, duplicate purchases/refunds, time/refund regression, over-refund and signed-BIGINT overflow refuse. Partial reversals use the difference between original remaining points before and after the cumulative refund, rather than rounding each refund independently. A33-unit refund against1999 units at3/100 reduces59 points to58; a full refund removes the remaining58. Original-season reversals cannot consume current-season points. Arithmetic alone establishes neither receipt authenticity nor owner/complete-query custody; the SQL owner and trusted sources establish those separately.
692
+
693
+ ## Durable and optional Loyalty
694
+
695
+ `./server` exports `dataStoreLoyalty`, `resolveLoyaltyStoreOptions` and five borrowed SQL roles: tenant fences, immutable definitions, subject fences/tail count, accepted entries and immutable seasons. It opens no native connection, uses no hot backend, reads/writes nothing in a tick and adds no CLI/Rooms boot hook. Complete ordered accepted entries are point truth and rebuild source; there is no materialized points cache. A durable per-subject end count rejects a missing history tail, in addition to sequence/owner/definition/season/arithmetic checks. Bounded complete history is required; archival expansion remains open.
696
+
697
+ Required `receiptSource.purchase/refund` methods must read actual accepted authority evidence and verify tenant, owner and canonical authority/purchase/refund identity. The source receives no player amount, weight, points or season. Purchase evidence carries the accepted amount/currency/kind/time and must refuse already-refunded or revoked purchases. Refund evidence carries the verified cumulative refunded amount. Explicit non-default rule version is mandatory. Sources/clock and strategy callbacks run outside replayable SQL callbacks. SQL serializes admission through tenant then subject fences and checks the unchanged complete tail before accepting an entry.
698
+
699
+ Purchase custody hashes tenant/logical loyalty ID/canonical authority/purchase independently of owner, content version and source aliases. Aliases and a new request nonce cannot credit the same receipt again. Refund custody likewise prevents replay/rebinding. Every reversal follows the original archived ratio, definition and season, even when current declarations have changed. Old-season refunds update the old season only. Seasonal programs require an explicit trusted `seasonSource`; accepted seasons retain immutable half-open bounds and reject changed identity or overlap. The current season view starts at zero without deleting old evidence or inventing a calendar. A logical program cannot silently change its decay mode after SQL admission.
700
+
701
+ `shopLoyaltyReceiptSource(store, { shop, refunds? })` reads an actual fulfilled default SQL Shop order and invokes its owning read-only original receipt service. It derives accepted virtual price and time from that receipt, never the current offer or a submitted price. Free/unsettled/foreign/mismatched orders refuse. This helper is for the default SQL Shop schema; a fully replaced Shop supplies its own receipt authority. The helper supplies no refund writer: omission of a verified refund source explicitly refuses reversals. It creates no automatic purchase consumer and does not make Shop purchase/refund and points one cross-system transaction. Catch-up, complete provider verification/refund settlement and cross-system consistency remain open.
702
+
703
+ `economyPlugin({ currencies, loyalty: { loyalty, version, receiptSource, seasonSource? } })` installs the default port, five roles, own `economy.loyalty`, `economy.loyalty.mine` and `economy.loyalty.receipt` queries, actual Tag and normal lifecycle/privacy hooks. `economy.economy.loyalty()` refuses while absent or inactive. There is no player credit/reverse/rebuild RPC. Those methods require a system Subject or a configured verified service; their captured intentions contain identities only. Player queries derive tenant/owner exclusively from Subject. `receipt` and trusted `rebuild` read accepted history without seeding rows, clock, current sources, slots or writes; missing is a nonterminal observation, never a new credit authority. Rebuild also works after current definitions are removed.
704
+
705
+ A complete `loyalty.service` replacement requires all eight receiver-bound methods, explicit non-default version/content and actual nonempty `querySources.list/mine/receipt` tables. Accessors refuse without invocation. It owns schema/CDC/receipts/season authority/privacy and has no default roles, bounds, source/strategy/clock options or fallback. Default sources may explicitly add actual app invalidation tables. Loyalty points are independent borrowed SQL truth over an explicitly supplied receipt source, so a replaced wallet does not acquire default wallet participants. Root real-money admission still applies. Time-only seasonal transitions require explicit refresh.
706
+
707
+ Export/erase owns all five roles after current content/currencies are removed. Erase clears owner/intent/evidence/point bodies and retains permanent subject fences, pseudonymous event identities, sequence/time and hashed purchase/refund custody. Shared immutable definitions and seasons survive erasure. Wallet, Shop and Inventory keep their own erase hooks; this port does not erase their values or infer merge continuity. A receipt cannot become fresh authority for another owner after erasure.
708
+
709
+ All four inspect layers and EN/DE panel show optional backend/version, tier/perks slots, source/season binding, redacted declaration type counts, conditional roles and SQL budgets or app ownership. They expose no owner, purchase reference, points, tier outcomes or refund evidence. Provider receipt verification, automatic consumption, perk effects and GM actions remain false. Shared/Local/Cloud read-only providers carry the same observation; failed refresh hides stale configuration and preserves reload focus. `useLoyaltyPrograms`, `useLoyalty` and `useLoyaltyReceipt` use actual browser-safe descriptors and perform no point writes or automatic recovery.
710
+
711
+ `./testing` supplies actual installed fresh memory-store `loyaltyPortFixture`, scoped `loyaltyFakeLayer`, a Tag-based port probe and real default tier/perks probes. The fixture uses an actual conserved virtual Shop purchase; its season/refund callbacks are explicitly test authority seams. Normal deactivation revokes escaped services.
712
+
713
+
714
+ ## Optional Piggy banks
715
+
716
+ The browser-safe `./piggy-banks` entry exports `definePiggyBank`, the complete
717
+ seven-method `PiggyBanksPort`, exported fill/reset defaults and one shared
718
+ `projectPiggyBank` history projection. Declarations capture ID/version/title,
719
+ an actual registry currency, an exact positive cap, unique `fillFrom` source
720
+ IDs, an unlock `Price` and `reset: 'onUnlock'`. The cap cannot exceed the
721
+ currency's wallet cap. `realMoney:false` refuses real/mixed declarations before
722
+ binding; virtual/free unlocks require no provider credentials. Configuration
723
+ bounds are admission budgets, not measured throughput.
724
+
725
+ Install through `economyPlugin({ currencies, realMoney: false, piggyBanks: {
726
+ banks, version, fillSource, unlockSource } })`. `fillSource.read` is required
727
+ server authority: accepted tenant/owner, canonical authority/event receipt ID,
728
+ source ID, exact amount and settlement time. `unlockSource.read` is required
729
+ trusted eligibility and price-adjustment authority. Neither source has an
730
+ invented Match/age/provider fallback. Inputs and receiver methods are captured
731
+ before lazy Effects; getters refuse without invocation. Application code owns
732
+ receipt authenticity, revocation and source invalidation.
733
+
734
+ Canonical fill custody is tenant + logical bank + authority + receipt ID,
735
+ independent of request nonce, source alias or submitted owner. Each player's
736
+ accepted reward needs its own canonical receipt ID: a shared Match ID alone
737
+ cannot identify multiple players' rewards. Rebinding one receipt to another
738
+ owner, even after erasure, conflicts. Only trusted system/allowed-service calls
739
+ can `fill` or `rebuild`; players never submit amount, fill state, Match result
740
+ or reset state. `defaultPiggyFill` grants at most accepted amount and remaining
741
+ cap; named custom fill rules may narrow that envelope only. A fill at cap records
742
+ zero delta and consumes its receipt permanently; replay after reset cannot
743
+ create delayed value.
744
+
745
+ Four borrowed SQL roles own tenant fences, immutable bank definitions, subject
746
+ fences/end count and accepted events. Complete ordered accepted fill/unlock
747
+ history is available-value/cycle truth and rebuild source; no mutable fill
748
+ cache exists. Durable end count refuses a missing tail. Per-bank sequence,
749
+ monotonic time, owner/definition/evidence and exact arithmetic are checked.
750
+ The first admitted logical bank ID fixes its complete normalized definition,
751
+ registry and rules version; changing them under that ID conflicts. Use a new
752
+ bank ID or an explicit application migration. Live content evolution and
753
+ archival migration are unfinished.
754
+
755
+ Player `unlock` sends observed bank ID/version/digest, original request ID and
756
+ optional virtual currency only. Eligibility, price and reset callbacks run
757
+ outside replayable SQL. Actual prepared wallet debit, wallet grant, accepted
758
+ reset and receipt commit in one exact-store group using the existing wallet
759
+ writer. Failure rolls back payment and accepted reset/grant together; pending
760
+ wallet admissions may remain. Default reset grants all available value and
761
+ retains zero; a named custom reset retains a remainder and grants only the
762
+ difference, preventing double credit. Current prices are declarations, not
763
+ pinned personalized quotes. Real settlement is explicitly refused by this
764
+ writer; enabling the global money switch supplies no provider implementation.
765
+
766
+ Accepted original fill/unlock retries and owning `receipt`/`rebuild` read only
767
+ historical SQL, without row seeding, current declaration/clock/source/slot or a
768
+ fresh transfer. `not-observed` is nonterminal, never proof of rollback. Privacy
769
+ works after declarations change: export returns owning bank/evidence/receipt
770
+ history; erase clears owner/body/digest, retains custody, fences and end count,
771
+ and preserves shared definitions. Wallet and other destinations use their own
772
+ privacy hooks. Merge, retention and complete cross-system forensic reconciliation
773
+ remain open.
774
+
775
+ Complete `piggyBanks.service` replacement captures all seven methods and requires
776
+ explicit nondefault version/content plus actual nonempty
777
+ `querySources.list/receipt` tables. It owns schema/SQL/CDC/source authority/privacy;
778
+ there are no default roles/bounds/source/clock/strategy options or fallback.
779
+ Default SQL Piggy banks refuse a replaced root wallet because they need its
780
+ actual prepared writer. A complete replacement pair may supply its own service.
781
+ Default query sources can add actual app fact invalidation tables.
782
+
783
+ The installed player queries/actions and `usePiggyBanks`, `useUnlockPiggyBank`,
784
+ `usePiggyBankReceipt` hooks are browser-safe intent/observation paths. There is
785
+ no player fill/rebuild RPC. Four inspect layers and EN/DE panel expose backend,
786
+ version, slots, redacted configured caps/source counts, exact conditional roles
787
+ and SQL bounds or app ownership. No private fills, balances, events or receipts
788
+ appear. Automatic filling, real settlement, durable client recovery and GM
789
+ capabilities remain false. Failed refresh hides stale observations while
790
+ retaining reload focus.
791
+
792
+ `./testing` supplies a fresh installed `piggyPortFixture`, scoped
793
+ `piggyBanksFakeLayer`, actual Tag probe and default/override fill/reset probes.
794
+ Fixtures use actual conserved wallet transfers; fill/eligibility are explicitly
795
+ named test authority seams.
796
+
797
+
798
+ ## Personalized, Flash and Bundle merchandising
799
+
800
+ `./offer-policies` exports browser-safe `definePersonalizedOffers`,
801
+ `defaultPersonalizedOfferSelector`, `resolvePersonalizedOffers`,
802
+ `defineFlashOffer`, `defaultFlashTriggerPolicy`, `resolveFlashOffer`,
803
+ `defineBundle` and `projectBundleValue`. These capture actual normalized Shop
804
+ content and the app currency registry. All `Price` declarations retain the
805
+ global real-money admission; `realMoney:false` supports virtual/free rules
806
+ without credentials and refuses real/mixed content. Enabling real references
807
+ never installs provider settlement. These rules introduce no tables, parallel
808
+ Shop/wallet writer, automatic Tick read or per-player state RPC.
809
+
810
+ A personalized pool declares ID/version, bounded slots, complete offers and
811
+ play-style affinity tags, plus explicit rotation anchor/duration. Trusted
812
+ context supplies canonical tenant/key, owned item IDs, play styles and time.
813
+ The default ranks unowned item kinds and matching declared styles, then uses
814
+ stable identity/rotation tie-breaking. Declaration order does not determine
815
+ selection. Rotation windows are half-open. A named selector override may choose
816
+ only unique declared offers within the slot bound; unknown/duplicate/excess
817
+ outputs refuse. Inputs and outputs are detached before lazy use. Canonical
818
+ identity across guest upgrade/merge and coherent fact snapshots belong to the
819
+ source; selection is not persisted exposure or account identity authority.
820
+
821
+ A Flash declaration captures one immutable one-time Shop offer, accepted trigger
822
+ kind and bounded duration. It forces single quantity/per-player lifetime one-time
823
+ limits; original Shop counters own once-only purchase. The trusted trigger
824
+ receipt includes canonical authority/event, tenant/owner, trigger kind and
825
+ accepted timestamp. Foreign, future or mismatched receipts refuse. The window
826
+ starts at that recorded time, is intersected with declaration availability and
827
+ never restarts from a refresh clock. The exported trigger default is replaceable
828
+ by a named narrowing slot; extending its envelope refuses. Missing explicit
829
+ trigger is unavailable, not fabricated eligibility. Event receipt durability,
830
+ revocation and selection of the original trigger remain source responsibilities.
831
+ A new window per later event is not a built-in rotation policy.
832
+
833
+ `offerPolicyContextSource` accepts optional `personalized`, `flashOffers`,
834
+ `personalizedSelector`, `flashTrigger` and explicit nondefault `policyVersion`.
835
+ Use that identity for the owning Shop rules version too. The required existing
836
+ facts source adds `personalization: { ownedItemIds, playStyles }` for pooled
837
+ offers and `flashTriggers: [{ flashId, trigger }]` for Flash offers. An explicit
838
+ null trigger means no window; omitted required records/facts refuse. Source
839
+ methods retain private receivers and getters refuse. Eligibility narrows the
840
+ existing app eligibility and composes with Starter/experiment rules. Both the
841
+ owning offer query and purchase use this same policy through ordinary Shop;
842
+ accepted original retries preflight historical SQL before current facts/clock,
843
+ even after expiry. No client ownership/style/trigger/window or amount is trusted.
844
+
845
+ `defineBundle({ offer, parts: [{ offer, qty }], showValue: 'sumOfParts' })`
846
+ requires parts to reproduce exactly the bundle's actual item/currency grants;
847
+ missing/duplicate/self-referential/mismatched parts refuse. Unit-price evidence
848
+ for `projectBundleValue` includes each part's ID/version, source reference,
849
+ observation time and normalized price. All parts must be represented once, with
850
+ explicit maximum age; stale/future/mismatched or amplified prices refuse.
851
+ The actual bundle price is admitted within its declaration too. Only matching
852
+ virtual currencies or real currency references are summed; absent comparability
853
+ produces no comparison, never an exchange rate. BigInt sums can exceed individual
854
+ SQL-wallet BIGINT bounds. `saving` is signed, so a more expensive bundle returns
855
+ a negative difference. Sum-of-parts display is not fabricated anchor-price
856
+ history, a pinned quote or legal/provider price verification.
857
+
858
+ `./server` adds `bundleValueFromShopReceipts({ shop, bundle, currencies,
859
+ version, maxPriceAgeMs }).read(subject, { at, bundlePrice, originals })`.
860
+ It captures the owning receipt method, checks verified own Subject and full
861
+ part content/rule digest, and reads accepted single-unit free or single-currency
862
+ virtual originals. Accepted price/time/order references supply the value;
863
+ missing/pending/future/stale/foreign/changed originals refuse. This adapter
864
+ accepts no real/mixed or multiple-alternative unit declarations. It reads no
865
+ current source/clock and creates no purchase or table. Purchase the bundle itself
866
+ through ordinary Shop. Verified refund state, provider/current-store prices,
867
+ atomic pricing snapshots and withdrawal/anchor history remain open.
868
+
869
+ The existing four-layer Shop configuration continues to label an explicit custom
870
+ context source; durable rotation/provider/LiveOps installation flags remain
871
+ false. These helpers do not install a new configuration page, player countdown,
872
+ comparison component, exposure event/ledger or automatic refresh. `./testing`
873
+ exports actual default/override selector and trigger probes.
874
+
875
+
876
+ ## Shared wallet spend limits and complete-tail custody
877
+
878
+ `./spend-limits` exports captured currency-specific daily/monthly/minor rules,
879
+ `defaultSpendLimitPolicy`, `resolveSpendBudget`, `utcSpendWindows`, `projectSpend`
880
+ and `assertSpendBudget`. Install `spendLimits: { limits, source, version, policy? }`
881
+ on the ordinary `economyPlugin` or `dataStoreEconomy` options. Limits use exact
882
+ nonnegative integer amounts; null means no configured cap. Minor caps are
883
+ intersected with general caps. Policy version must be explicit and nondefault.
884
+ Named custom policy can only narrow caps and permission. No exchange rate or
885
+ cross-currency total is invented. Only declared limit currencies are covered.
886
+
887
+ Required `source.read({ tenantId, ownerId, ownerKind, currency, at })` returns
888
+ those exact scope fields, actual `allowed`/`minor` facts, `observedAt`, optional
889
+ parent daily/monthly caps and optional `blockedUntil`. Foreign/future/malformed
890
+ facts refuse; no adult, age or parental-consent fallback exists. Private source
891
+ receivers are captured and accessors refuse. Author source/policy callbacks run
892
+ outside replayable SQL. Fact freshness, revocation and atomic changes of an
893
+ external parental/profile system remain source responsibilities; a timestamp
894
+ field alone is not verification. A block timestamp is a generic restriction,
895
+ not a statutory cooling-off, consent, cancellation or refund implementation.
896
+
897
+ The existing prepared wallet writer owns the debit check under its ordinary
898
+ subject/currency fences. Shop, paid Crafting, Codes, Containers, Subscriptions,
899
+ Piggy unlocks and server transfers using that actual wallet participant share
900
+ this guard; there is no new Shop wrapper or separate counter cache. Complete
901
+ accepted owner/currency postings and their real paired transfer receipts supply
902
+ spend truth. UTC days and calendar months are half-open; credits/refunds do not
903
+ silently restore consumed allowance. Exact totals are recomputed in the SQL
904
+ commit callback and compared with captured policy, so multiple units in one
905
+ transaction cannot use the same remaining budget twice. Rejection rolls back
906
+ all accepted values/postings/counts/receipts in the group; pending admissions may
907
+ remain. Accepted original retries return before current source/clock/policy.
908
+
909
+ The existing subject role now retains `entriesCount` for the whole owner's
910
+ ledger across currencies. Each accepted wallet posting increments it in the same
911
+ transaction. Writer, audit/rebuild and unerased privacy require a complete
912
+ bounded owner history with that count; a deleted tail is refused rather than
913
+ rebuilding/spending from a prefix. `maxLedgerRows` therefore bounds complete
914
+ owner history, including all currencies. Erasure keeps the count and permanent
915
+ fence while removing owning fields; shared financial values remain under the
916
+ existing privacy policy. Rebuild repairs materialized wallets, never a missing
917
+ source ledger/count. Missing/invalid count is not automatically inferred from a
918
+ potentially incomplete history.
919
+
920
+ A fully replaced root wallet refuses default `spendLimits` options and owns its
921
+ own debit/source/SQL/privacy contract. Default configuration needs no extra SQL
922
+ role or secret and works with virtual currency. Provider payments, cross-Store/
923
+ Stripe real-money aggregation, debt/refund/withdrawal policy and complete legal
924
+ parent controls remain open. These options do not grant a player transfer route.
925
+
926
+ All four inspect layers and EN/DE section report configuration only: rules
927
+ version, policy/source, UTC period, complete-history budget and configured
928
+ currency/minor caps. No owner, actual minor status, parental facts, spend totals,
929
+ ledger or provider references appear. Default SQL root required; unknown/private
930
+ extras, relaxed minors and claimed provider/cooling/parent UI capability refuse.
931
+ Existing read-only host providers/reload/error/focus are reused; failure hides
932
+ stale caps. Provider/cooling/parent UI installation flags remain false.
933
+
934
+ `./testing` exports actual default/override policy probes, fresh installed
935
+ `spendLimitsPortFixture`, scoped `spendLimitsFakeLayer` and actual `EconomyPort`
936
+ Tag probe using the same writer. Named fixture age/parent facts are test seams;
937
+ normal deactivation revokes escaped wallets.
938
+
939
+ ## Trade intentions and replaceable contract
940
+
941
+ `@voltro/plugin-economy/trades` contains captured `defineTradeRules`,
942
+ `tradeProposeIntent`, `tradeAcceptIntent`, `tradeCancelIntent`,
943
+ `resolveTradeEligibility`, `resolveTradeTax` and the full eight-method
944
+ `TradesPort`. The Tag alone installs no player routes or panels. The trusted four-role SQL
945
+ adapter is described below.
946
+
947
+ A proposal selects immutable item instance quantities and declared game-currency
948
+ consideration, target (or public target `null`), bounded lifetime and request ID.
949
+ Those terms never establish ownership or balances. Acceptance sends only the
950
+ observed trade ID/digest and request ID. Cancellation sends only ID/request ID.
951
+ Unknown player state and duplicate assets refuse. Real-money trading has no
952
+ channel. The actual owning service must bind the admitted terms and original
953
+ lifetime to durable custody before producing a receipt.
954
+
955
+ Trusted participant facts are tenant/owner-bound, include explicit permission,
956
+ TrustScore and actual item quantity/tradability/expiry/lock/reservation identity.
957
+ The default permits an already locked item only for this exact trade reservation;
958
+ foreign locks refuse. TrustScore has to come from the actual sanctions authority;
959
+ this module never invents it. Eligibility and named overrides can only narrow
960
+ permission, target, lifetime, ownership and availability. The adapter must reread
961
+ actual materialization under transaction fences; these facts are not a lock.
962
+
963
+ Default tax deducts the configured basis points from the receiver's gross amount,
964
+ rounding upward with exact integers. Gross equals net plus tax; an override may
965
+ reduce the declared tax only. The same-writer Wallet participant below owns the
966
+ actual net/tax sink pairs; the durable Trade service below binds that projection to
967
+ reservations, transfer, expiry, original replay and owner privacy. A tax projection does not move or mint value.
968
+
969
+ `./testing` exports `tradeFixture`, actual default/override
970
+ `tradeTaxSystemFixture` and `tradeEligibilitySystemFixture`. Installed Trade
971
+ Escrow/Inventory binding, owning SQL journal, replacement/scoped Layer, hooks,
972
+ player RPC and four inspect layers are described below. Automatic expiry, client
973
+ journal/components, marketplace search/fee and native parity remain open.
974
+
975
+ ## Same-writer game-currency Wallet Escrow
976
+
977
+ Default `EconomySqlService` and the installed `economy.economy` facade expose
978
+ `prepareEscrowReserve`, `prepareEscrowRelease`, `prepareEscrowSettle`,
979
+ `prepareEscrowTaxSettle` and read-only `escrow`. Participant/input/observation
980
+ and `WalletEscrowSettlementReceipt` types are exported from
981
+ `@voltro/plugin-economy/server`. These require trusted system or explicitly
982
+ allowed-service authority. The four-method request EconomyPort stays unchanged.
983
+ Replaced root wallets cannot borrow the default SQL participant. The facade
984
+ captures inputs without invoking getters and refuses after normal deactivation.
985
+
986
+ Reserve takes owner, tenant, permanent reservation ID, declared game currency
987
+ and exact positive amount. The existing prepared Double-Entry writer debits
988
+ available wallet currency and credits the canonical per-owner/reservation/
989
+ currency system Escrow account. Only a private same-writer capability can use
990
+ `escrow.` system accounts; ordinary `transfer` and `prepareTransfer` refuse
991
+ minting or draining that namespace, including `escrow.tax.`. Actual key/digest
992
+ generation is shared. Spend/minor/parent limits apply to the reserve debit;
993
+ release never restores period allowance. No provider or room tick I/O is used.
994
+
995
+ Nullable server-only transaction `escrow` custody binds immutable reserve terms
996
+ and the accepted close mode, opaque target fence and actual receipts. Reserve
997
+ postings/custody and closing postings/custody each commit in one exact-store
998
+ group. Release returns the whole amount; ordinary settle pays the whole gross
999
+ to a different wallet. Tax settle takes an explicit nonnegative exact `taxAmount`
1000
+ and optional reserved-prefix `taxAccountId` (default `escrow.tax.default`). The
1001
+ Trade service must derive that amount through `resolveTradeTax`; this trusted
1002
+ participant does not invent a tax rate. Gross equals net plus tax. Net is an
1003
+ actual wallet payout; positive tax is a separate actual Double-Entry system
1004
+ pair to the bound sink. Both use the existing writer. No alternate wallet or
1005
+ financial source is installed.
1006
+
1007
+ `WalletEscrowSettlementReceipt` reports gross/net/tax and actual nullable
1008
+ `payout`/`taxReceipt`. Tax zero has no tax receipt. Tax equal to the entire
1009
+ reserved amount has no payout receipt or recipient wallet posting. No zero-value
1010
+ or synthetic financial receipt is fabricated. Recipient admission/privacy fences
1011
+ still apply. Recipient cap checks net; payout, tax, custody, counts and accepted
1012
+ receipts roll back together. A failed preparation may leave pending admission
1013
+ rows; pending opposite child intents are not evidence of a paid close.
1014
+
1015
+ Release, gross settle and tax settle share one permanent closing identity.
1016
+ Deterministic owner/currency/reserve fences and actual paired evidence admit
1017
+ only one target/mode/tax/sink. Conflicting retries refuse. Tax-zero and gross
1018
+ settle return the same actual payout original. Accepted retries precede current
1019
+ clock/spend callbacks; original reserve after close never resets or re-debits.
1020
+ Missing accepted custody/close marker, wrong amount/account/owner/time/reference
1021
+ or absent paired evidence refuses instead of reopening or inferring payment.
1022
+ The bounded prepared-unit budget applies to all descendants. Erased source or
1023
+ recipient refuses execution; originals do not bypass privacy fences. No partial
1024
+ close, FX, fee or real-money trading is performed.
1025
+
1026
+ `escrow` returns `not-observed`, `held`, `released` or `settled` after validating
1027
+ custody and real fund/payout/tax pairs, without seed, clock, sources, repair or
1028
+ writes. Accepted observations carry nullable `closingReceipt` and `taxReceipt`.
1029
+ Ordinary complete Double-Entry history/count rebuild remains wallet truth.
1030
+ Missing immutable Escrow custody is refused. Source-owned close observation can
1031
+ survive recipient erasure using original receipts and retained redacted financial
1032
+ pairs. Opaque fences are pseudonymous, not a legal anonymization claim.
1033
+
1034
+ Existing user/guest export includes own `escrows` only when present. Owning erase
1035
+ clears Escrow custody/receipts/digests, wallet identity and paired reference/actor
1036
+ fields, including the source-owned system-only tax transaction. Financial deltas
1037
+ and permanent owner fences remain. Later reserve/close cannot recreate an erased
1038
+ owner. Unsettled erased funds stay inaccessible; refund/disposition, guild
1039
+ lifecycle, arbitrary actor/reference erasure and legal retention require explicit
1040
+ policy. Flow reports show wallet-boundary `escrow.*` sources/sinks, not a newly
1041
+ minted supply claim. Complete Escrow/tax analytics classification remains open.
1042
+
1043
+ The written `realMoney:false` path needs no provider credentials. Inventory's
1044
+ four inspect layers retain `tradingInstalled:false`; these supporting
1045
+ participants do not install a player Trade service. Installed Trade
1046
+ binding/RPC/four panels are described below. Sanctions/eligibility, automatic
1047
+ expiry, components, replacement/scoped port conformance, recovery/consent and
1048
+ templates remain open.
1049
+
1050
+ ## Atomic fresh counterparty and Trade Escrow composition
1051
+
1052
+ `EconomySqlService.prepareEscrowReserveTaxSettle` and the default-only revocable
1053
+ facade capture `WalletEscrowReserveTaxSettleInput`. They reserve the exact fresh
1054
+ amount and settle its net/tax in one actual SQL group. The returned
1055
+ `WalletEscrowFundedSettlementReceipt` contains real `reserve` and `settlement`
1056
+ receipts. Accepted originals precede current callbacks; changed
1057
+ amount/target/tax/sink refuses. No separate wallet writer or second database is
1058
+ used.
1059
+
1060
+ `./server` exports `tradeEscrowParticipant` and its structural types. It captures
1061
+ immutable trusted tenant/trade/proposer/offered/requested game-currency/item
1062
+ terms and the actual prepared Wallet and optional Inventory participants. The
1063
+ three methods `prepareReserve`, `prepareRelease`, `prepareExchange` require
1064
+ trusted system/explicit service authority and a mandatory exact-store prepared
1065
+ SQL admission. That admission belongs to the durable Trade writer and actual
1066
+ sanctions/eligibility authority. It is not optional, a player payload or a new
1067
+ clock/source check inside a replayable SQL callback. It must fence and revalidate
1068
+ the current proposal/expiry/participant/consent/eligibility state. The helper does
1069
+ not fabricate those facts or a durable Trade journal.
1070
+
1071
+ Reserve locks the proposer's actual selected items and moves actual available
1072
+ currency to its original Escrow. Exchange requires existing offered custody
1073
+ with the exact selected quantities/amounts. It transfers original or split items
1074
+ and closes offered currency; requested items and currency use compound fresh
1075
+ reserve/transfer and reserve/net/tax participants in the same group. Actual
1076
+ `resolveTradeTax` captures exact deductions outside SQL. Release frees only the
1077
+ proposer's original offered holdings. No parallel asset writer, new SQL table,
1078
+ real-money channel, exchange rate or newly minted Trade value is introduced.
1079
+
1080
+ The whole group includes required admission and every prepared descendant.
1081
+ Failure rolls back both owners' item/lock/history and wallet/fund/payout/tax/
1082
+ custody/receipt effects together; pending admissions can remain. Exact-store
1083
+ mismatch and work-budget overflow refuse before commit. Currency-only and
1084
+ empty one-sided legs are explicit; items require the actual Inventory participant.
1085
+ The normal per-unit recipient caps/capacity still apply in execution order;
1086
+ final-position capacity/netting for full inventories is not inferred. This
1087
+ fresh helper refuses already transferred/released offered custody. The durable
1088
+ Trade writer must return its accepted original before invoking it again.
1089
+
1090
+ The test admission exercises actual owner/store fencing only; it is not
1091
+ production sanctions, consent or Trade persistence proof. These fixtures use
1092
+ actual existing writers, real rollback and exact tax projection.
1093
+
1094
+ Installed lifecycle/Tag/RPC/four-layer binding of the durable eight-method
1095
+ `TradesPort` is described below. Expiry/recovery workflow, actual sanctions source,
1096
+ marketplace/gifting, player components, participant conformance and templates remain open.
1097
+ Inventory's existing `tradingInstalled:false` is retained. Trusted asset
1098
+ composition alone does not install player trading or complete `realMoney:false`.
1099
+
1100
+ ## Durable SQL Trade adapter
1101
+
1102
+ `@voltro/plugin-economy/server` exports `dataStoreTrades`, `TradeStoreOptions`,
1103
+ `TradesSqlService` and four `tradeTables` roles: tenant fence/accepted-trade count,
1104
+ subject fence/accepted-operation count, proposal/closing custody and permanent
1105
+ request operations. The adapter implements all eight `TradesService` methods
1106
+ and a separate trusted `expire`. The request port stays eight methods.
1107
+ This is a trusted raw adapter; `economyPlugin` does not install it, its tables,
1108
+ Tag, player RPC or panels yet. Existing `tradingInstalled:false` stays accurate.
1109
+
1110
+ Options require declared game currencies, explicitly versioned Trade rules,
1111
+ actual same-store prepared Wallet/optional Inventory participants and
1112
+ `TradeAuthoritySource`. `authority.read` provides actual tenant/owner-bound
1113
+ permission, trust score and inventory facts. `authority.prepare` returns a
1114
+ required prepared SQL admission that revalidates those facts and owns actual
1115
+ revocation/eligibility/consent/expiry fences. Both callbacks run outside replayable
1116
+ SQL; the returned unit executes inside the Trade group. No default trust score,
1117
+ parental consent, sanctions adapter or permitted actor is fabricated. The named
1118
+ `eligibility` override only narrows the existing default Trade policy. An
1119
+ application source must integrate its real authority; fixture seams are no proof
1120
+ of that integration or of commit-time expiry freshness.
1121
+
1122
+ Proposal inputs are detached intentions; no balance, ownership or accepted-state
1123
+ payload is accepted. `captureTradeProposeIntention` captures pure bounded wire
1124
+ shape. An accepted original is compared before current currencies/rules/source/
1125
+ clock admission. Fresh proposals still validate actual declarations and bounds.
1126
+ Immutable terms include target or public target null, original lifetime,
1127
+ currencies/rules and exact captured tax projection. Offered items and currency
1128
+ are held through the actual existing writers in the same group as proposal
1129
+ custody and request receipt. Target privacy fences apply. Public proposals do not
1130
+ fabricate an unknown accepter; marketplace/listing fee/search remains separate.
1131
+
1132
+ Views disclose rule version and each offered/requested gross/net/tax, with
1133
+ server-derived nullable accepter ID. The digest binds original terms and tax.
1134
+ Accept sends only that trade ID/digest/request ID. Actual participant facts and
1135
+ prepared authority admission must permit the original lifetime/target/items.
1136
+ The same Trade tenant/owner fences admit one permanent closing mode/participant;
1137
+ both actual Escrow legs, taxes, journals/counts/custody and Trade receipt commit
1138
+ together. Cancel releases the proposer holdings without fresh eligibility facts.
1139
+ Trusted `expire` uses one canonical request after the captured deadline. No timer,
1140
+ cron, workflow scheduler or automatic cleanup is installed by this raw adapter.
1141
+
1142
+ Matching accepted retries validate SQL custody and actual original Inventory and
1143
+ Wallet evidence before current callbacks, including when the winner commits
1144
+ during another call's preparation. Changed kind/terms/digest/target or conflicting
1145
+ close refuses. Missing accepted operation/trade tail, original custody or paired
1146
+ asset evidence refuses instead of reseeding or reopening. Permanent accepted
1147
+ markers distinguish erased pending admissions from accepted trade counts;
1148
+ request/owner sequences retain a complete bounded accepted tail. Pending requests
1149
+ are not accepted value. Whole tenant/owner work exceeding the configured budget
1150
+ refuses. Execution-order asset caps/capacity remain conservative.
1151
+
1152
+ `mine` is tenant/participant scoped; it does not reveal all public listings.
1153
+ `receipt` is actor/request scoped and read-only. `rebuild` repairs only mutable
1154
+ Trade state from complete accepted proposal/closing/request custody and validated
1155
+ asset evidence; it never repairs missing history, redebits or re-grants assets.
1156
+ Wallet/Inventory own their materialization rebuilds. No current source callback
1157
+ or hidden cache becomes truth. Complete runtime CDC/query invalidation is still
1158
+ part of the installed binding.
1159
+
1160
+ User/guest export includes owned/targeted/accepted Trade custody and own requests.
1161
+ Erase clears related proposal/closing and all copied counterpart Trade receipt
1162
+ bodies, own identity and pending payloads, preserving permanent opaque fences/
1163
+ counts/markers and financial asset truth in the owning plugins. Repeated erase
1164
+ remains valid. Unsettled erased funds/locks require explicit disposition and
1165
+ coordinated owning-plugin privacy policy; this hook invents no refund or transfer.
1166
+ A partial peer Inventory erase can leave no full peer receipt for Trade proof;
1167
+ that incomplete proof is refused until coordinated privacy completes. Opaque
1168
+ keys are pseudonymous, not legal anonymization or retention decisions.
1169
+
1170
+ Native sources apply all four actual table roles and use independent participant
1171
+ pools. Shared canonical JSON serialization reuses the existing Shop
1172
+ implementation so JSON key order does not change declaration hashes.
1173
+
1174
+ Installed lifecycle/Tag/complete replacement/player query/action/four-layer
1175
+ binding is described below. Events/Mixins, actual sanctions/consent authority,
1176
+ expiry/ recovery workflows, public marketplace/gifting, player components and
1177
+ templates remain open.
1178
+
1179
+ ## Installed Trade system
1180
+
1181
+ `economyPlugin({ currencies, trades: { rules, authority, inventory } })` now
1182
+ installs the actual eight-method `TradesPort`, conditional four Trade table roles,
1183
+ normal bind/deactivate/export/erase hooks and five browser-safe player routes.
1184
+ `authority` is required for the default SQL adapter: its real scoped facts and
1185
+ exact-store prepared revalidation unit must own sanctions, trust, consent and
1186
+ stock freshness. No default permission or production sanctions source is supplied.
1187
+ The installed Inventory plugin's `inventory` facade can provide the actual item
1188
+ participant; omit it only for currency-only trades. Default SQL Trades over a
1189
+ fully replaced root wallet refuses before binding. No room/tick or uninstalled
1190
+ boot path is added.
1191
+
1192
+ The optional `trades` configuration inherits declared currencies, root clock and
1193
+ trusted service names. Explicit `rules.version` is required. Named `eligibility`
1194
+ and `taxPolicy` slots reuse actual defaults and may only narrow admission/tax.
1195
+ `maxHistoryRows` bounds the complete tenant Trade history and each owner request
1196
+ history; it is a configured refusal budget, not capacity evidence. The shared
1197
+ `resolveTradeStoreOptions` captures participants/configuration before binding,
1198
+ including private method receivers, and rejects getters without invoking them.
1199
+
1200
+ A complete `trades.service` replacement must implement all eight owning methods,
1201
+ supply versioned `rules` and actual `querySources: { mine, receipt }`. It owns
1202
+ schema, authority, receipts, CDC and privacy. Default authority, item participant,
1203
+ clock, work-bound and policy options are refused with a replacement. No default
1204
+ Trade roles, SQL budgets or fallback are installed. The plugin still applies
1205
+ verified subject, own intention and trusted rebuild guards to captured methods;
1206
+ normal deactivation revokes escaped/lazy facade calls. In-progress operations
1207
+ are not claimed to be cancelled by deactivation.
1208
+
1209
+ Player RPCs are `economy.trades.mine`, `economy.trades.receipt`,
1210
+ `economy.trades.propose`, `economy.trades.accept`, `economy.trades.cancel`.
1211
+ `./web` exports `useTrades`, `useTradeReceipt`, `useProposeTrade`,
1212
+ `useAcceptTrade`, `useCancelTrade`. Proposals choose terms/target/lifetime/request;
1213
+ acceptance chooses the observed original digest/request; cancellation chooses
1214
+ trade/request. No balance, owner, permission, tax or resulting state is accepted.
1215
+ Views disclose original gross/net/tax, rule version and server-derived accepter.
1216
+ Wire schemas validate exact amounts and coherent tax/target/state disclosure.
1217
+ Queries observe original SQL plus actual asset receipt sources; explicit app fact
1218
+ sources may extend default sources. Currency-only installation omits Inventory
1219
+ sources. No client recovery journal or automatic retries are provided by hooks.
1220
+
1221
+ `plugin.economy.expireTrade` is trusted server-only for the default adapter. It
1222
+ uses the same permanent closing identity and actual original release; before the
1223
+ captured expiry it refuses. A full replacement exposes no synthetic expiry
1224
+ method/fallback. No player expiry route or automatic expiry scheduler is installed. The explicit
1225
+ app-owned batch/schedule factory is described below.
1226
+
1227
+ Server `/status`, Shared UI, Cloud decoder/proxy and the Local read provider
1228
+ share strict `tradesInstalled`/nullable `tradesSystem`, actual backend/rule/slot/
1229
+ authority/item participant/budget and exact conditional table roles. The bilingual
1230
+ configuration section shows configured tax, lifetime and minimum trust only;
1231
+ never player facts, subjects, terms, operations, assets or receipts. Replacements
1232
+ report app-owned authority/items, custom slots and null SQL budgets. The existing
1233
+ reload/failure/focus/provider path is reused; no new GM write capability appears.
1234
+ Public marketplace, automatic expiry, GM actions and client recovery remain false.
1235
+
1236
+ `./testing` exports fresh installed `tradesPortFixture`, scoped
1237
+ `tradesFakeLayer` and an actual `tradesPortSystemFixture` probe over the same
1238
+ wallet and Inventory writers. Named permission/trust fixture seams are not
1239
+ production sanctions or consent. Full generic `systemContractConformance`,
1240
+ Events/Mixins, production sanctions/consent/freshness, automatic
1241
+ expiry/recovery, marketplace/ gifting, coordinated erase/disposition, player
1242
+ components and templates remain open.
1243
+
1244
+ ## Explicit bounded Trade expiry schedule
1245
+
1246
+ Default `TradesSqlService.expireDue` and installed `plugin.economy.expireDueTrades`
1247
+ accept trusted server subjects plus `{ tenantId, maxTrades? }`. `maxTrades`
1248
+ defaults to 1000 and is a refusal budget within the existing complete-history
1249
+ bound. A user or inactive/replaced/unconfigured default system refuses. Inputs
1250
+ and the runner's private receiver are captured before lazy execution. No player
1251
+ batch/expiry route, tick read or new SQL table is introduced.
1252
+
1253
+ The runner captures its clock outside SQL, reads the complete bounded tenant
1254
+ Trade snapshot, validates permanent accepted counts, proposal/closing/receipt
1255
+ metadata and distinct owner tails, then selects only proposed deadlines at or
1256
+ before that captured instant. The same captured snapshot and distinct-owner
1257
+ validation are reused during selection. Erased custody stays frozen and is not
1258
+ reopened. More due trades than `maxTrades` refuses before the first close;
1259
+ missing history or malformed metadata cannot be treated as an empty backlog.
1260
+ There is no unproved pagination cursor, archive or repaired-history shortcut.
1261
+
1262
+ Each selected trade closes through actual existing `expire` and original asset
1263
+ release in its own complete transaction. A competing accepted/cancelled/expired
1264
+ closing is counted only after re-reading valid durable Trade custody and actual
1265
+ asset evidence; a still-open or erased/corrupt failure stays failed. An
1266
+ interrupted batch may have committed earlier individual closes: the entire
1267
+ firing remains failed, and explicit retry selects remaining original custody.
1268
+ This is not a batch-wide transaction or exactly-once cron delivery claim.
1269
+
1270
+ Successful output contains counts and `capturedAt` only: `scannedRows`,
1271
+ `dueTrades`, `expiredTrades`, `alreadyClosedTrades`. `expiredTrades` counts
1272
+ matching original expiry receipts observed by this invocation; parallel jobs
1273
+ can both observe one receipt. It does not mean a count of fresh financial
1274
+ writes. Repeated invocation after closure sees no due trade. No owner, terms,
1275
+ request identity, asset or receipt body is returned by the batch report.
1276
+
1277
+ `./server` exports ordinary app-owned `tradeExpirySchedule({ tenantId, runner,
1278
+ maxTrades?, name?, cron?, timezone? })`. The `runner` can be the installed
1279
+ `plugin.economy` facade. Default cron is `* * * * *` in UTC; registration is
1280
+ explicit through the existing app schedule mechanism. The handler uses a
1281
+ server system subject, invokes the real runner and fails the firing for errors
1282
+ or inconsistent counts. The plugin itself registers no automatic job, so the
1283
+ configuration panel still reports `automaticExpiryInstalled:false`. Full
1284
+ replacements receive no default batch/expiry fallback.
1285
+
1286
+ Accepted owner-history validation now also checks every operation's tenant,
1287
+ owner key/identity, admission, kind/state, permanent request ID/digest and exact
1288
+ receipt shape even when the requested original belongs to a different trade.
1289
+ Per-trade operation selection reads the complete bounded set and selects
1290
+ accepted sequence markers; pending opposing requests do not hide a later
1291
+ accepted receipt behind a fixed four-row prefix. No missing tail/count is inferred.
1292
+
1293
+ The direct schedule-body fixture is not execution by the scheduler engine or a
1294
+ durable cron/crash proof.
1295
+
1296
+ ## Explicit client Trade intention recovery
1297
+
1298
+ `./web` exports `createTradeRecovery`, `browserTradeIntentionJournal`,
1299
+ `captureTradeIntention` and the public journal/slot/reference/transport/recovery
1300
+ contracts. The transport supplies the three installed propose/accept/cancel
1301
+ Actions; propose receives canonical string amounts, suitable for the wire schema.
1302
+ A caller supplies a stable app plus authenticated tenant/account scope without
1303
+ credentials and updates it on every account/session/target change. The server
1304
+ still derives real owner/tenant from its authenticated Subject; a client scope
1305
+ is not proof of authority.
1306
+
1307
+ A saved `TradeIntention` is one of `{ kind:'propose', input }`,
1308
+ `{ kind:'accept', input }`, `{ kind:'cancel', input }`. Only captured intentions
1309
+ are persisted, with version1 and scope. Selected item references, quantities and
1310
+ consideration are intentionally part of proposal recovery, never statements of
1311
+ ownership/balance, player facts, trust, secrets or received asset state. The
1312
+ browser namespace is `voltro:trade-intention:v1:<scope>` with a configured
1313
+ 32768-character refusal bound. It stores one unresolved intention per scope;
1314
+ no expiry deletes an uncertain request and no new nonce replaces it.
1315
+
1316
+ `begin` captures and requires a matching storage read acknowledgement before
1317
+ transport is possible. `retry(original)` acquires the actual cross-tab Web Lock,
1318
+ rechecks saved scope/operation/terms/request and holds that lock through response
1319
+ and settlement. It sends only the saved original, never automatically replays
1320
+ or invents a request ID. Failure keeps the acknowledged record. Every
1321
+ `selectScope` invalidates queued/in-flight continuations, including switching
1322
+ away and back; a late accepted response cannot clear the old original under a
1323
+ new session. Explicit observation can later reconcile that original.
1324
+
1325
+ `observe` accepts detached `TradeObservation`; not-observed stays unresolved.
1326
+ Only a matching original receipt may clear. Proposal receipts must match target,
1327
+ complete offered/requested terms and original duration with state proposed;
1328
+ accepted receipts match trade/digest with accepted state; cancellation matches
1329
+ trade with cancelled state. All check matching request/observation identity and
1330
+ complete coherent gross/net/tax, lifetime, target/accepter and rule shape.
1331
+ A proposal's original receipt remains proposed even if its current trade later
1332
+ closes; a current list snapshot is not the proposal operation receipt. Missing,
1333
+ changed, malformed or erased evidence never grants dismissal authority.
1334
+
1335
+ Pure `captureTradeView`, `captureTradeReceipt`, `captureTradeObservation` in
1336
+ `./trades` reuse one shape validator across RPC, owning SQL request history and
1337
+ client recovery. They reject getters/unknown fields and validate disclosure;
1338
+ shape validation is not server asset/permission proof. Missing owner history
1339
+ cannot become an empty accepted tail. Shop and Trade now use one internal
1340
+ acknowledgement/scope/lock/storage/retry algorithm; Shop retains its public
1341
+ contracts, original namespace and domain-specific purchase matching. No parallel
1342
+ Shop implementation or alternative financial writer remains.
1343
+
1344
+ The caller must provide actual browser storage and Web Locks. Missing capabilities
1345
+ refuse; there is no memory fallback. Factory-provided journals must honor the
1346
+ same cross-tab and durable acknowledgement contract. Captured journal/transport/
1347
+ storage/lock methods preserve private receivers and refuse accessors. Hooks do
1348
+ not automatically construct a journal, select identity, retry, or add a player
1349
+ screen. The server configuration observation therefore still reports
1350
+ `clientRecoveryInstalled:false`; explicit app integration is required.
1351
+
1352
+ The deterministic lock/storage seam does not prove actual browser cross-tab,
1353
+ storage-event, engine SDK or crash behavior. Player recovery components are
1354
+ described below; template/consumer integration, production sanctions and
1355
+ consent, gifting/public marketplace and coordinated privacy/disposition remain
1356
+ open; no acceptance, capacity or complete virtual-only stack claim.
1357
+
1358
+ ## Player Trade disclosure and recovery components
1359
+
1360
+ `./components` exports `TradeTerms`, `TradeList`, `TradeAcceptDialog`,
1361
+ `TradeIntentionRecovery`, their prop contracts and bilingual `tradeUiCopy`.
1362
+ They extend the existing semantic UI/dialog/button system and accept app-owned
1363
+ source, action, journal and content-label callbacks. No transport, credential,
1364
+ financial writer or new visual system is embedded in the components.
1365
+
1366
+ `TradeTerms` captures a coherent original server view before displaying proposer
1367
+ and counterparty item selections plus gross/net/tax for each game currency,
1368
+ including a zero net payout. Unknown or inconsistent disclosure shows an error.
1369
+ Instance/currency label callbacks resolve actual app content; the component
1370
+ never invents an item identity, price, permission or trust score. Terms are
1371
+ original server disclosure, not a live wallet or ownership report.
1372
+
1373
+ `TradeList` renders own/targeted/accepted views only for its current `actorId`;
1374
+ foreign participant observations and duplicate/invalid views refuse. This is
1375
+ presentation scoping, not server authority. A failed or loading source hides
1376
+ stale actions. Review/Cancel are guarded by original state and actor selection;
1377
+ new review additionally requires an observed lifetime that is still open.
1378
+ Deadline display uses the chosen locale, and time is supplied explicitly; no
1379
+ hidden server clock or per-tick read is introduced. The source list has a
1380
+ configured 1000-view refusal bound, not a support/capacity claim.
1381
+
1382
+ `TradeAcceptDialog` uses the existing native modal with labeled title/help,
1383
+ both sides and explicit original acceptance intention. Confirmation requires
1384
+ matching trade/digest, actor, proposed state, original open lifetime and no
1385
+ busy/error/result state. The callback sends only that captured intention;
1386
+ the parent acknowledges it through recovery before the actual Action.
1387
+ An accepted original is displayed only when request/trade/digest/accepter and
1388
+ all immutable terms/dates/rules/taxes match; a late response after the displayed
1389
+ deadline remains valid while a new acceptance stays disabled. Close/ESC never
1390
+ clears a saved uncertain request. Native stacking/focus behavior needs later
1391
+ browser evidence.
1392
+
1393
+ `TradeIntentionRecovery` belongs above transient lists, errors and dialogs. It
1394
+ checks reference version/expected account scope and captured operation before
1395
+ showing an unresolved request; a foreign or unauthenticated scope hides it
1396
+ without deleting the durable record. It shows selected proposal consideration
1397
+ without inventing tax before a server proposal receipt exists. Recovery reference
1398
+ is secondary in a disclosure. Retry and optional receipt-check controls invoke
1399
+ only saved original callbacks; there is no dismiss, new nonce or silent storage
1400
+ clear. Busy controls remain mounted/focusable with guarded `aria-disabled`.
1401
+ Raw exception/provider bodies are not exposed to players.
1402
+
1403
+ They use actual installed wallet/Inventory Trade writers and the journal;
1404
+ native-dialog methods and storage/locks remain explicit DOM seams, not proof of
1405
+ keyboard stacking, responsive/mobile, actual transport or cross-tab browser
1406
+ behavior. A complete app session/provider/journal parent integration and
1407
+ templates remain open.
1408
+
1409
+ ## Inventory-backed Trade authority
1410
+
1411
+ `inventoryTradeAuthoritySource(store, { permissions, inventory, items })` from
1412
+ `@voltro/plugin-economy/server` composes the actual bound Inventory service with
1413
+ an explicit `TradePermissionSource`. The permission source must provide both
1414
+ `read` and `prepare`; there is no default allowance, trust score, adult status or
1415
+ consent. Its read returns tenant/owner-bound `allowed` and `trustScore`. Its
1416
+ prepared SQL unit must fence the owning policy records and revalidate those
1417
+ captured facts. A read-only Sanctions port cannot fulfill this atomic contract.
1418
+ Until that integration exists, leave Trades uninstalled or fail admission.
1419
+
1420
+ The helper reads actual own possessions and original reservation evidence.
1421
+ Tradability comes from explicitly supplied item contracts keyed by item ID and
1422
+ schema version. An unknown contract refuses. Another trade's lock never becomes
1423
+ this trade's reservation. The existing actual Inventory/Wallet writers still
1424
+ revalidate stock, expiry and conservation inside the same commit; a shaped
1425
+ client object or the helper's read alone never establishes admission.
1426
+
1427
+ `captureTradeAuthorityAdmission` captures one proposer or two distinct accepting
1428
+ participants, the original trade/tenant, phase and open lifetime. The policy
1429
+ receives detached permission facts rather than item attributes or author-owned
1430
+ arrays. The helper admits its prepared unit against the exact supplied SQL
1431
+ store before any writer executes. A foreign-store unit refuses. Policies must
1432
+ implement their actual sanctions, consent and privacy fences; the helper creates
1433
+ no policy table, automatic sanctions installation or synthetic permission.
1434
+
1435
+ The source fixtures contain a clearly named test-only app-policy table and use
1436
+ actual borrowed SQL plus Inventory/Wallet writers. Five source scenarios and one
1437
+ scenario per six native variants cover custody, revocation between read and
1438
+ commit, full rollback, receiver/getter admission, removed contracts, unavailable
1439
+ facts and original receipt replay after revocation.
1440
+
1441
+ ## Actual SQL sanctions in Trade admission
1442
+
1443
+ `sanctionsTradePermissionSource(store, sanctions.sanctions.admission())` from
1444
+ `@voltro/plugin-economy/server` supplies the required policy for
1445
+ `inventoryTradeAuthoritySource`. It calls the actual server-only prepared
1446
+ Sanctions capability at enforcement point `trade`, with the Trade writer's
1447
+ captured time. Each participant receives real active-measure and conservative
1448
+ strike-derived trust checks; no `allowed:true`, trust bonus or policy table is
1449
+ invented by the adapter. Receiver-safe methods and detached input admission
1450
+ preserve tenant/owner binding. Permission units must belong to the same SQL store
1451
+ as Inventory, Wallets and Trades. The returned guarded group revalidates actual
1452
+ sanction records atomically with the assets.
1453
+
1454
+ Actual `tradeBan`, general bans, insufficient trust, unavailable service, erased
1455
+ identity and changed prepared records refuse new acceptance. A participant may
1456
+ still cancel an existing hold through the ordinary Trade release path. Accepted
1457
+ original proposal/accept receipts remain recoverable before current policies,
1458
+ never grant fresh authority. Policy errors retain redacted forbidden/conflict
1459
+ semantics across the SQL boundary. Proposal and acceptance acquire policy before
1460
+ Trade/asset admission. Additional consent or legal gates require their own real
1461
+ prepared policy; this adapter establishes sanctions only.
1462
+
1463
+ The `api-economy` source factory now installs actual Sanctions, versioned
1464
+ Trades, Inventory and Wallets in one application. Binding supplies the exact
1465
+ borrowed store to these helpers; there is no substitute wallet, fake sanction
1466
+ service, player balance write or provider credential. An explicit bounded app
1467
+ schedule expires held proposals.
1468
+
1469
+
1470
+ ## Explicit IAP verification foundation
1471
+
1472
+ `./iap` is browser-safe and exports
1473
+ `defineIapCatalog`, `iapReceiptIntent` and `iapProductsCsv`. A product explicitly
1474
+ maps an existing captured Shop offer, channel and consumable/non-consumable/
1475
+ subscription kind to the offer's real or mixed store SKU. Duplicate SKU or offer
1476
+ mappings per channel refuse. Non-consumables and subscriptions require one unit; non-consumables also require a per-player cap of one.
1477
+ The catalog preserves the actual offer grants; it never provisions store products.
1478
+ CSV exports identifiers and kinds for console preparation, without receipts,
1479
+ reference prices or credentials. A sync-products CLI is not installed yet.
1480
+
1481
+ An empty catalog works with `realMoney:false` without provider configuration.
1482
+ Nonempty real catalogs and all verification adapters refuse when money is disabled.
1483
+ The receipt intent contains only channel, offer ID/version, request ID and bounded
1484
+ opaque receipt. Tenant, owner, amount, transaction identity, quantity and payment
1485
+ state are not player inputs. Do not log raw receipt evidence.
1486
+
1487
+ The server-only `IapVerificationPort`, `iapVerificationPort`,
1488
+ `appleIapVerificationPort` and `googleIapVerificationPort` require explicit trusted
1489
+ account authority. `IapAccountSource.read` returns the server-owned stable account
1490
+ binding for the authenticated tenant/owner and provider environment. It must be
1491
+ unique across all tenants sharing an application. Never accept that binding from
1492
+ an RPC input. The generic replacement is deliberate server authority responsible
1493
+ for authenticity, fresh provider status and account binding; its output shape
1494
+ alone proves none of those properties.
1495
+
1496
+ Apple uses the official server library with supplied DER trust roots and online
1497
+ certificate checks. It verifies the submitted StoreKit 2 JWS, requests its current
1498
+ transaction from the App Store Server API and verifies that JWS too. The two
1499
+ transaction projections must agree. Bundle, environment, SKU, type and
1500
+ `appAccountToken` must match; family sharing, revocation, upgraded subscriptions,
1501
+ advanced commerce and malformed data refuse. Production requires `appAppleId`.
1502
+ Only Sandbox and Production are admitted; unsigned Xcode/LocalTesting modes are
1503
+ excluded. `AppleIapCredentialSource.read` supplies rotated App Store Connect
1504
+ signing key/key ID/issuer ID. Missing credentials or verification failures never
1505
+ switch provider or fulfill a payment. The upstream contracts are in the
1506
+ [Apple library](https://github.com/apple/app-store-server-library-node).
1507
+
1508
+ Google reads the fixed authenticated [Products v2 endpoint](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.productsv2/getproductpurchasev2)
1509
+ using an explicitly supplied server-side service-account OAuth source with
1510
+ androidpublisher authority. HTTP has abort/time/byte budgets and refuses redirects.
1511
+ A successful purchase must match the server account and exactly one configured
1512
+ product. Pending/cancelled, partially refunded, rental/preorder and ambiguous
1513
+ multi-line purchases refuse. Sandbox requires an explicit test-card purchase;
1514
+ production rejects test purchases. Consumption and acknowledgement observations
1515
+ are retained. They do not authorize regranting an already processed purchase.
1516
+ Google subscriptions remain separate unfinished work.
1517
+
1518
+ `VerifiedIapPurchase` is opaque process-local evidence; copying or serializing it
1519
+ loses its verification capability. Transaction custody identity includes channel,
1520
+ environment, application and provider transaction, independent of request, tenant
1521
+ or owner. Google tokens are hashed, never exposed as transaction IDs. Apple paid
1522
+ amounts retain their provider milliunit scale. Google Products v2 supplies no paid
1523
+ amount here, so `paid` remains null. Reference prices never fill that gap or produce
1524
+ Loyalty spend credit. Evidence alone does not grant assets, record an order,
1525
+ establish consent or implement privacy/retention.
1526
+
1527
+ The explicit SQL adapter described below now owns durable receipt custody and
1528
+ atomic fulfillment. Optional lifecycle/RPC/four-layer configuration binding is
1529
+ described below; `paymentsInstalled` reports configuration only. Provider roots,
1530
+ credentials, products and sandbox accounts are operator decisions. No capacity
1531
+ claim follows from the configured work budgets.
1532
+
1533
+
1534
+ ## Explicit SQL IAP fulfillment
1535
+
1536
+ `dataStoreIap(store, options)` borrows the app store and supplies the four-method
1537
+ `IapService`/`IapPort`: redeem, receipt, exportSubject and eraseSubject. It requires
1538
+ `realMoney:true`, an immutable operator-owned namespace, matching currencies,
1539
+ verification service, mandatory prepared SQL purchase admission, actual prepared
1540
+ Wallet/Inventory participants and an explicit source account. It opens no store,
1541
+ registers no timer and changes no boot path. `captureIapService` binds all four
1542
+ replacement methods, including private class receivers, without executing getters.
1543
+ The optional Economy plugin IAP binding described below installs this adapter.
1544
+
1545
+ Four server tables own namespace work counts, subjects, orders and global receipt
1546
+ custody. The receipt primary key derives from provider application/environment/
1547
+ channel/transaction independently of tenant, owner, request and work namespace.
1548
+ Pending verified custody binds the original order permanently; aborted groups may
1549
+ retain pending admissions, never accepted value. A fresh alias or owner/tenant/
1550
+ namespace rebind returns `already_redeemed`. The exact accepted original request
1551
+ returns its stored receipt before current provider, clock, policy or participant
1552
+ callbacks. Changed intentions conflict. Raw receipt evidence is hashed and never
1553
+ stored in order snapshots or exposed in player receipts.
1554
+
1555
+ Fresh fulfillment requires opaque verification capability, exact matching currency
1556
+ configuration and a prepared `IapPurchaseAdmissionSource`. The application must
1557
+ provide actual age/consent/spend authority under its own SQL fences; no permissive
1558
+ facts or consent default exists. Namespace, subject and receipt fences, admission,
1559
+ actual wallet transfers, catalog inventory grants and accepted order/custody commit
1560
+ as one exact-store group. A foreign-store participant, revoked policy, capacity
1561
+ failure or malformed returned receipt rolls back accepted state. Fresh evidence
1562
+ already consumed or acknowledged refuses; historic originals still recover first.
1563
+ Subscriptions refuse here until their entitlement implementation exists.
1564
+
1565
+ One-time/per-player, UTC-day and global quantity limits use whole bounded accepted
1566
+ SQL history. Non-consumable declarations require a single unit and per-player cap
1567
+ of one. Accepted offer/channel/version configuration digests remain immutable even
1568
+ after body erase. Accepted offer quantities/days retain global consumption after
1569
+ erasure. Namespace and subject row counts detect missing history; exceeding the
1570
+ complete work budget refuses instead of scanning a prefix or repairing from a
1571
+ partial selection. No process lock or second monetary writer is introduced.
1572
+
1573
+ Export includes own pending/fulfilled order observations without raw receipts.
1574
+ Erase removes owner links and full order/provider bodies, fences recreation and
1575
+ retains permanent receipt custody, counters and minimal quantity metadata. These
1576
+ keys are pseudonymous, not an anonymization claim. Wallet and Inventory own
1577
+ their separate export/erase hooks; this adapter neither erases those systems nor
1578
+ invents refunds, retention periods or disposition of paid assets.
1579
+
1580
+ They exercise real Wallet/Inventory writes, original recovery, custody,
1581
+ rollback, SQL-policy revocation, erasure and missing-history refusal.
1582
+
1583
+
1584
+ ## SQL-owned IAP account bindings
1585
+
1586
+ `dataStoreIapAccounts(store, options)` implements the server-only five-method
1587
+ `IapAccountsPort`/`IapAccountBindingService`: read, observe, prepareBinding, exportSubject and
1588
+ eraseSubject. Its real-money/application/channel/environment configuration is
1589
+ captured once. `read` captures the server-selected tenant/owner, mints a public
1590
+ pseudonymous UUID at runtime and persists one stable binding under the actual SQL
1591
+ owner fence. A second process/pool reads that same token. Different owners,
1592
+ tenants, applications, environments and channels have distinct records; requesting
1593
+ a channel/environment outside the configured service refuses. Tokens are account
1594
+ identifiers for StoreKit/Play, not secrets or authentication credentials.
1595
+
1596
+ Two SQL roles own account bodies and global token-uniqueness custody. Account keys
1597
+ bind actual application/channel/environment/tenant/owner; token custody is global
1598
+ within application/channel/environment, independent of tenant. Entropy is captured
1599
+ before replayable SQL, never supplied by the player. Erase removes owner/token
1600
+ bodies, marks custody erased and permanently refuses recreation. Export returns
1601
+ only the selected binding. Retained keys are pseudonymous, not anonymous. Account
1602
+ merge, migration, external-token adoption and retention are unfinished.
1603
+
1604
+ `IapAccountSource` now requires both read and prepareBinding. Verification evidence
1605
+ privately retains its captured account source/token; no account token enters
1606
+ player evidence, order snapshots or financial receipts. The default IAP SQL
1607
+ writer prepares that binding and puts its exact-store guard before purchase
1608
+ admission and asset writes. Account erasure after provider verification therefore
1609
+ refuses the fresh commit. Accepted originals are read from their financial SQL
1610
+ receipt first and can recover after separate account erasure until order privacy
1611
+ removes their body. Do not treat a read-only account lookup as atomic admission.
1612
+ Complete account replacements own their real prepared SQL authority and privacy.
1613
+
1614
+ Standalone SQL adapters require their own account and financial privacy wiring.
1615
+ Storage/account guards use actual SQL; their provider verifier remains a
1616
+ deliberate synthetic seam.
1617
+
1618
+
1619
+ ## Installed IAP readiness, privacy and configuration
1620
+
1621
+ `economyPlugin({ currencies, realMoney: true, iap: { version, products, providers,
1622
+ namespace, admission, inventory, systemAccount } })` now installs optional IAP.
1623
+ The default SQL binding requires explicit immutable version/namespace/source
1624
+ account, actual prepared age/consent/spend admission and actual Inventory
1625
+ preparation when products grant items. It reuses the installed default Wallet;
1626
+ default SQL fulfillment over a replaced root Wallet refuses. Subscription
1627
+ products refuse this default installation until entitlement fulfillment exists.
1628
+
1629
+ Each provider declares channel, sandbox/production and actual application ID,
1630
+ plus a synchronous server `create(context)` factory. Channels are unique; all
1631
+ products need a provider and each provider needs products. The captured context
1632
+ contains only that channel's actual catalog, currencies, app/environment, clock
1633
+ and borrowed-store account registry. Factories return complete initialize/verify
1634
+ services. Verified app/environment/catalog/currency evidence must match the
1635
+ installed declarations. Provider shape is not authentication authority; use the
1636
+ actual Apple/Google adapters or an explicitly trusted server replacement.
1637
+
1638
+ ```ts
1639
+ import {
1640
+ googleIapVerificationPort,
1641
+ type GoogleIapAccessTokenSource,
1642
+ type IapProviderDefinition,
1643
+ } from '@voltro/plugin-economy/server'
1644
+
1645
+ // Example application identity only; provision the actual app and SKU in the store console.
1646
+ export const playProvider = (
1647
+ accessTokens: GoogleIapAccessTokenSource,
1648
+ ): IapProviderDefinition => ({
1649
+ channel: 'android',
1650
+ environment: 'production',
1651
+ applicationId: 'studio.example.game',
1652
+ create: ({ realMoney, environment, applicationId, products, currencies, accounts, now }) =>
1653
+ googleIapVerificationPort({
1654
+ realMoney, environment, applicationId, products, currencies, accounts, now,
1655
+ accessTokens,
1656
+ }),
1657
+ })
1658
+ ```
1659
+
1660
+ Binding constructs adapters without reading credentials or importing the Apple
1661
+ SDK. Only configured IAP contributes `onActivate`; it awaits every captured
1662
+ provider's required `initialize` before exposing IAP operations. Apple reads and
1663
+ validates supplied App Store Connect credentials and constructs its real SDK
1664
+ trust verifier. Google reads and admits the supplied server token. These checks
1665
+ are setup admission, not proof of remote authorization, token expiry or a valid
1666
+ purchase. Every verification reads the credential source again for rotation.
1667
+ Missing required credentials or defective roots fail initialization; no unsigned,
1668
+ fake credential, automatic provider switch or fallback is supplied. Deactivation
1669
+ revokes the facade; a late initializer cannot reopen a deactivated generation.
1670
+
1671
+ `economy.economy.iap()` returns initialize/account plus the four-method IapPort
1672
+ facade. `economy.iap.account` is an Action: verified own user/guest selects channel
1673
+ only, the server persists/returns its public account UUID. `economy.iap.redeem`
1674
+ is an Action accepting only `IapReceiptIntent`. Preserve the original request on
1675
+ uncertain responses. Do not log raw receipts; retain the Audit plugin's input
1676
+ redaction. Neither Action wraps the owning adapters in another transaction.
1677
+ `useIapAccount` and `useRedeemIap` are in `./web`.
1678
+
1679
+ `receipt` now takes `IapReceiptReference`: channel/offer/version/request and a
1680
+ SHA-256 receipt digest, never the raw receipt. `createIapReceiptReference(intent)`
1681
+ in `./iap` captures the input before Web Crypto hashing the canonical JSON string
1682
+ representation used by SQL. `iapReceiptReference` admits stored references.
1683
+ `economy.iap.receipt` and `useIapReceipt` use this read-only hash reference; they
1684
+ never verify, seed or grant. Missing/pending observations remain nonterminal;
1685
+ a changed reference conflicts. This is not a durable recovery journal.
1686
+
1687
+ Installed export composes own account and financial observations. Erase first
1688
+ revokes account bindings, then removes financial bodies, before the root Wallet
1689
+ hook erases its owner. Destination Inventory keeps its own installed privacy
1690
+ hook. The sequence is retryable, not an atomic cross-plugin erase or paid-asset
1691
+ refund/disposition policy. Removed account/receipt identities remain fenced.
1692
+
1693
+ A complete `IapSystemService` replacement implements initialize/account/redeem/
1694
+ receipt/complete/exportSubject/eraseSubject, supplies captured products/version and actual
1695
+ receipt query sources, and forbids all default SQL options. It owns provider,
1696
+ account, schema, authority, CDC, grants, conservation, activation and privacy;
1697
+ there are no default IAP roles, borrowed default Wallet, SQL budgets or fallback.
1698
+ Private class receivers work; getters and partial services refuse.
1699
+
1700
+ The four configuration layers show exact optional six SQL roles, provider
1701
+ applications, product mappings, binding/source/version and work budgets. Full
1702
+ replacement has no provider declaration list or default work bounds. The
1703
+ `paymentsInstalled`/`iapInstalled` flags report configured receipt fulfillment,
1704
+ not provider health or successful purchases. Explicit completion authority is
1705
+ reported per channel below. Notifications and refund debt are conditional on the
1706
+ explicit optional binding below; subscription entitlements, GM actions and
1707
+ installed client recovery remain false.
1708
+
1709
+
1710
+ ## Explicit provider completion after accepted SQL
1711
+
1712
+ `dataStoreIap` now additionally exposes server-only
1713
+ `IapSqlService.prepareCompletion(subject, originalIntent)`. This is a read-only
1714
+ accepted-original path: own verified subject, exact request/evidence digest,
1715
+ fulfilled order, complete accepted body and matching global SQL custody. Missing,
1716
+ pending, changed, erased or corrupt evidence refuses. No order is seeded, grant
1717
+ replayed, clock consulted or provider called. The result is an opaque
1718
+ `AcceptedIapCompletion` capability with order ID only. Raw receipt remains in its
1719
+ private captured resolver; JSON/shape copying cannot restore authority. Exercising
1720
+ the capability re-reads the owning SQL original, so later erasure/removal refuses
1721
+ before provider I/O. This is not an atomic lock spanning SQL and a remote store.
1722
+
1723
+ `IapCompletionPort`/`IapCompletionService` has required initialize/complete methods.
1724
+ `iapCompletionPort` captures a deliberate trusted server completion authority,
1725
+ actual catalog/app/environment/currencies and account source. It admits the real
1726
+ accepted SQL capability and current account before provider completion. It does
1727
+ not verify a new purchase, fulfill, reverse money or accept player state. Complete
1728
+ replacements own actual provider observations; output shape alone proves nothing.
1729
+ Private receivers are captured, required getters refused, defects redacted.
1730
+
1731
+ ```ts
1732
+ import { Effect } from 'effect'
1733
+ import type {
1734
+ IapSqlService,
1735
+ IapCompletionService,
1736
+ } from '@voltro/plugin-economy/server'
1737
+ import type { IapReceiptIntent } from '@voltro/plugin-economy/iap'
1738
+ import type { Subject } from '@voltro/protocol'
1739
+
1740
+ export const completeAccepted = (
1741
+ orders: IapSqlService,
1742
+ provider: IapCompletionService,
1743
+ subject: Subject,
1744
+ originalIntent: IapReceiptIntent,
1745
+ ) => Effect.gen(function* () {
1746
+ const evidence = yield* orders.prepareCompletion(subject, originalIntent)
1747
+ return yield* provider.complete(evidence)
1748
+ })
1749
+ ```
1750
+
1751
+ `googleIapCompletionPort` takes the same operator-owned options as the Google
1752
+ verifier. Its credential source is read on every invocation; explicit initialize
1753
+ checks setup admission only. Completion reuses the verifier's bounded fixed
1754
+ Products-v2 GET. Current owner, application, environment, SKU, quantity, purchase
1755
+ time/value and token identity must match the accepted original. Cancellation,
1756
+ partial refunds or changed purchase identity refuse before a provider POST.
1757
+ Consumables use the official [consume endpoint](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products/consume);
1758
+ non-consumables use [acknowledge](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products/acknowledge),
1759
+ with an empty body or empty JSON respectively. No developer payload, nonce or
1760
+ new transaction is minted. Existing terminal provider state needs no POST.
1761
+
1762
+ After a successful or uncertain POST, one fresh authenticated GET must observe
1763
+ matching consumed/acknowledged state. POST response alone is not completion;
1764
+ nonterminal state returns unavailable while accepted SQL assets/custody remain.
1765
+ An explicit same-capability/original-intention retry observes current state and
1766
+ never grants again or automatically sends a second POST after one uncertain call.
1767
+ HTTP uses redirect refusal, abort/time/byte budgets and redacted failures. No
1768
+ reference price is converted into paid amount or Loyalty credit.
1769
+
1770
+ This standalone port is implemented. Installed completion/RPC and encrypted
1771
+ notification/refund binding are described below. StoreKit client finish,
1772
+ subscriptions, Steam/Web and full workflow integration remain separate open
1773
+ work. Configuration panels report the actual configured completion factory.
1774
+ Synthetic HTTP/OAuth fixtures use only newly generated transient test material;
1775
+ they are not real Google authorization, sandbox purchase or production evidence.
1776
+
1777
+
1778
+ ## Installed explicit provider completion
1779
+
1780
+ A provider may additionally declare `createCompletion(context)`. Its captured
1781
+ initialize/complete service is constructed during binding without I/O, and its
1782
+ initializer is awaited during IAP activation. Missing/failed readiness keeps the
1783
+ IAP facade unavailable. A channel without completion authority has no implied
1784
+ completion fallback. The complete system replacement now requires seven methods,
1785
+ including `complete(subject, originalIntent)`, and owns its completion lifecycle.
1786
+
1787
+ The installed facade prepares a read-only opaque capability from the exact own
1788
+ accepted SQL order, then delegates to the configured channel authority. A
1789
+ changed receipt/owner/request cannot finish another purchase. It never redeems
1790
+ again, invokes the purchase verifier, grants items or posts Wallet value.
1791
+ Deactivation before handoff to the authority refuses. No-IAP applications
1792
+ receive no additional activation hook or completion route.
1793
+
1794
+ ```ts
1795
+ import { economyPlugin } from '@voltro/plugin-economy'
1796
+ import {
1797
+ googleIapProvider,
1798
+ type IapPluginOptions, type GoogleIapAccessTokenSource,
1799
+ } from '@voltro/plugin-economy/server'
1800
+ import { Effect } from 'effect'
1801
+
1802
+ const installPlayEconomy = (
1803
+ currencies: Parameters<typeof economyPlugin>[0]['currencies'],
1804
+ iap: Omit<IapPluginOptions, 'providers' | 'service' | 'querySources'>,
1805
+ applicationId: string,
1806
+ accessTokens: GoogleIapAccessTokenSource,
1807
+ ) => economyPlugin({
1808
+ currencies, realMoney: true, iap: {
1809
+ ...iap, providers: [googleIapProvider({
1810
+ environment: 'sandbox', applicationId, accessTokens,
1811
+ })],
1812
+ },
1813
+ })
1814
+
1815
+ const completeOriginal = (
1816
+ installed: ReturnType<typeof installPlayEconomy>,
1817
+ subject: Parameters<ReturnType<typeof installed.economy.iap>['complete']>[0],
1818
+ originalIntent: Parameters<ReturnType<typeof installed.economy.iap>['complete']>[1],
1819
+ ) => Effect.gen(function* () {
1820
+ const service = installed.economy.iap()
1821
+ const accepted = yield* service.redeem(subject, originalIntent)
1822
+ const observedCompletion = yield* service.complete(subject, originalIntent)
1823
+ return { accepted, observedCompletion }
1824
+ })
1825
+ ```
1826
+
1827
+ The account, prepared purchase-admission source, item participant, catalog and OAuth
1828
+ source are actual application inputs, not supplied credentials or permissive policy
1829
+ defaults. The example targets sandbox declarations; a real test purchase/credential
1830
+ is still required for validation. Provider completion is a remote operation after
1831
+ SQL acceptance, not one distributed transaction. If it fails or its reply is lost,
1832
+ retain the original accepted financial receipt and retry **only complete** with the
1833
+ same intention. Never mint another request or redeem another grant to finish it.
1834
+ `economy.iap.complete` is an explicit Action mounted only for configured completion
1835
+ or a complete replacement. `useCompleteIap` exposes it without automatic retries or
1836
+ a journal. It takes the original opaque mutation receipt, no player completion
1837
+ state; keep audit input redacted. A returned observation is not durable SQL proof
1838
+ of provider state and contains no raw receipt, account token or credential.
1839
+
1840
+ All four configuration layers now report `providerCompletionInstalled` and each
1841
+ configured provider's `completionInstalled`. These report declaration bindings,
1842
+ not readiness, healthy credentials or successful purchases. A complete
1843
+ replacement owns its implementation and has no default provider list. Google
1844
+ completes by a fresh matching authenticated terminal read, including after
1845
+ uncertain POST. StoreKit transaction finishing remains an explicit native-client
1846
+ responsibility; no server-only `finish` callback substitutes for it.
1847
+
1848
+ Synthetic finish authority proves no StoreKit purchase.
1849
+
1850
+ `googleIapProvider({ applicationId, environment, accessTokens, timeoutMs?,
1851
+ maxResponseBytes? })` captures one installed Play declaration and creates both
1852
+ actual verification and explicit completion adapters. It supplies no OAuth
1853
+ token, price or permissive purchase policy. Provider construction does not call
1854
+ the credential source. The installed context carries captured product
1855
+ definitions; the helper removes its channel before strict Google options
1856
+ admission.
1857
+
1858
+
1859
+ ## IAP hash-reference recovery
1860
+
1861
+ `./iap-recovery` now supplies `createIapPurchaseRecovery`, the journal/source/
1862
+ transport interfaces and `browserIapPurchaseJournal`. One journal entry stores only
1863
+ version, stable authenticated app/tenant/account scope and the original
1864
+ channel/offer/version/request/receipt SHA256 reference. No raw receipt, OAuth token,
1865
+ provider account UUID, amount, grant or completion state is saved. Scope keys must
1866
+ contain no credentials. An actual store/secure receipt source must implement
1867
+ `restore(reference)`; a reference cannot reconstruct or authorize a purchase.
1868
+
1869
+ ```ts
1870
+ import {
1871
+ browserIapPurchaseJournal, createIapPurchaseRecovery,
1872
+ type IapReceiptRecoverySource, type IapPurchaseRecoveryTransport,
1873
+ } from '@voltro/plugin-economy/iap-recovery'
1874
+ import type { IapReceiptIntent } from '@voltro/plugin-economy/iap'
1875
+
1876
+ const connectRecovery = (
1877
+ accountScope: string,
1878
+ actualStoreReceipts: IapReceiptRecoverySource,
1879
+ transport: IapPurchaseRecoveryTransport,
1880
+ ) => {
1881
+ const recovery = createIapPurchaseRecovery(
1882
+ browserIapPurchaseJournal(localStorage, navigator.locks),
1883
+ actualStoreReceipts,
1884
+ transport,
1885
+ )
1886
+ recovery.selectScope(accountScope)
1887
+ return recovery
1888
+ }
1889
+
1890
+ const submitSelectedPurchase = async (
1891
+ recovery: ReturnType<typeof connectRecovery>,
1892
+ originalIntent: IapReceiptIntent,
1893
+ ) => {
1894
+ const saved = await recovery.begin(originalIntent)
1895
+ return recovery.retry(saved.request)
1896
+ }
1897
+ ```
1898
+
1899
+ `begin` captures the original, computes its hash and requires matching durable
1900
+ storage acknowledgement before any server mutation. `pending` only reads; it does
1901
+ not restore, purchase or finish. User-triggered `retry(saved.request)` first reads
1902
+ the original hash-only SQL observation. Accepted SQL evidence skips current
1903
+ verification and another redemption. Missing/pending reuses the exact original
1904
+ intention after the actual source restores evidence matching **every** saved field
1905
+ and hash. Never generate a new request to recover a purchase.
1906
+
1907
+ The explicit completion callback receives the original and its actual accepted
1908
+ financial receipt. For configured Play it calls the installed complete Action; for
1909
+ StoreKit an actual native adapter must call and observe transaction.finish after SQL
1910
+ acceptance. Successful callback output must match receipt order, channel and time.
1911
+ Neither callback shape nor a synthetic finish is a provider-authenticity proof.
1912
+ Financial `fulfilled` alone, missing/pending and uncertain redemption/completion all
1913
+ keep the journal entry. Only matching completed observations clear it. No automatic
1914
+ retry, purchase, acknowledgement or finish is scheduled.
1915
+
1916
+ The coordinator reuses the existing intention acknowledgement/lock/settlement core.
1917
+ It holds the actual supplied lock through read, restore, hash, mutation, completion
1918
+ and clearing, rechecking scope generation between asynchronous boundaries. Every
1919
+ scope selection invalidates queued work, including away and back. A scope change
1920
+ cannot cancel an already dispatched remote operation; its original journal remains
1921
+ for explicit recovery. Another reference conflicts until the original settles.
1922
+ Missing Web Locks/storage and failed acknowledgement refuse, with no memory fallback.
1923
+
1924
+ Native implementations provide actual durable acknowledgement and matching
1925
+ process/ client locking through IapPurchaseJournal. The configuration flag
1926
+ clientRecoveryInstalled remains false because these app/engine bindings are not
1927
+ installed by the server plugin. This exported source coordinator does not
1928
+ establish any of those implementations.
1929
+
1930
+ Browser-safe receipt admission now shares exact canonical paid units, asset IDs,
1931
+ positive transfer amounts, unique child receipts/instances and valid timestamps
1932
+ with the default SQL receipt decoder and server completion result admission. It
1933
+ decodes observations only; actual own SQL/provider authority remains required.
1934
+ No decoded object alone grants assets or completes providers.
1935
+
1936
+
1937
+ ## Full installed IAP port and scoped fixtures
1938
+
1939
+ The installed server layer now exposes `IapSystemPort` with all seven methods:
1940
+ initialize, account, redeem, receipt, complete, exportSubject and eraseSubject.
1941
+ `captureIapSystemService` captures real private class receivers and refuses partial
1942
+ methods/accessors. The ordinary browser-safe `IapPort` keeps its four financial/
1943
+ privacy methods; both Tags resolve the same installed owning facade. A full system
1944
+ replacement omits default accounts/SQL layers, while no configured IAP contributes
1945
+ neither IAP Tag. Readiness/privacy/lifecycle admission still belongs to the owning
1946
+ system; resolving a Tag does not independently authorize a purchase.
1947
+
1948
+ ```ts
1949
+ import { Effect } from 'effect'
1950
+ import { IapSystemPort } from '@voltro/plugin-economy/server'
1951
+ import { iapFakeLayer } from '@voltro/plugin-economy/testing'
1952
+
1953
+ const observeFixtureAccount = Effect.scoped(Effect.gen(function* () {
1954
+ const iap = yield* IapSystemPort
1955
+ const subject = {
1956
+ type: 'user' as const, kind: 'guest' as const,
1957
+ id: 'fixture.account', tenantId: 'fixture',
1958
+ }
1959
+ return yield* iap.account(subject, 'ios')
1960
+ }).pipe(Effect.provide(iapFakeLayer)))
1961
+ ```
1962
+
1963
+ `./testing` exports `iapPortFixture`, fresh scoped `iapFakeLayer`,
1964
+ `iapPortSystemFixture` and `iapSystemPortSystemFixture`. These use actual
1965
+ installed SQL account/order/custody/Wallet/Inventory services with explicit
1966
+ synthetic verifier/ finish authorities and real lifecycle release. Their probes
1967
+ resolve actual Tags, replay original SQL receipts without another grant,
1968
+ complete accepted orders and observe permanent account erase fences. Scoped
1969
+ release revokes escaped facades. Fixtures supply no real provider credential,
1970
+ crypto purchase or permissive production policy.
1971
+
1972
+ These are concrete port fixtures for Plan21, not a complete generic
1973
+ `systemContractConformance` registration. Full strategy
1974
+ defaults/events/mixins/all- table privacy/inspect/stages registration, app-owned
1975
+ replacement integration, compiled ordinary boot and every other remaining
1976
+ acceptance requirement stay open.
1977
+
1978
+
1979
+ ## Installed Apple declaration
1980
+
1981
+ `appleIapProvider({ applicationId, environment, appleRootCertificates, appAppleId?,
1982
+ credentials })` captures the existing actual StoreKit verifier as an installed IAP
1983
+ declaration. It uses the same application/environment/root/credential admission as
1984
+ `appleIapVerificationPort`, copies the deployment's public root bytes before lazy
1985
+ binding, and captures the actual credential-source receiver. Construction does not
1986
+ read credentials, import the SDK or contact Apple. Production requires the numeric
1987
+ Apple app ID as well as the application bundle ID. Missing real credentials fail
1988
+ activation; no generated third-party credential or unsigned purchase mode exists.
1989
+
1990
+ ```ts
1991
+ import { economyPlugin } from '@voltro/plugin-economy'
1992
+ import {
1993
+ appleIapProvider,
1994
+ type IapPluginOptions, type AppleIapCredentialSource,
1995
+ } from '@voltro/plugin-economy/server'
1996
+
1997
+ const installAppleEconomy = (
1998
+ currencies: Parameters<typeof economyPlugin>[0]['currencies'],
1999
+ iap: Omit<IapPluginOptions, 'providers' | 'service' | 'querySources'>,
2000
+ applicationId: string,
2001
+ trustedDeploymentRoots: readonly Uint8Array[],
2002
+ credentials: AppleIapCredentialSource,
2003
+ ) => economyPlugin({
2004
+ currencies, realMoney: true, iap: {
2005
+ ...iap, providers: [appleIapProvider({
2006
+ environment: 'sandbox', applicationId,
2007
+ appleRootCertificates: trustedDeploymentRoots, credentials,
2008
+ })],
2009
+ },
2010
+ })
2011
+ ```
2012
+
2013
+ The factory removes the installed channel before the strict Apple options decoder
2014
+ and supplies actual captured product definitions. It does not add a server completion
2015
+ factory. StoreKit transaction.finish belongs to an actual native client **after** the
2016
+ matching SQL grant has been accepted; its recovery source/finisher can use the
2017
+ explicit hash-reference coordinator above. The inspect channel correctly reports
2018
+ completionInstalled:false for this declaration; this is not an unfinished HTTP
2019
+ fallback to a server-only finish callback.
2020
+
2021
+ Their public root construction seam is not an Apple trust chain; missing
2022
+ credentials refuse before SDK construction. No new table, event, credential,
2023
+ support or capacity claim is introduced.
2024
+
2025
+
2026
+ ## Authenticated notification ports
2027
+
2028
+ `appleIapNotificationPort` uses the existing SDK to verify Notifications-v2 and nested
2029
+ signed transactions/renewals against captured deployment roots and app/environment.
2030
+ `googleIapNotificationPort` uses the shared RS256/JWKS verifier with actual configured
2031
+ HTTPS audience, service-account email, verified-email claim and exact subscription.
2032
+ RTDN supports one-time, subscription, voided, pending-review and test hints; it does
2033
+ not declare sandbox/production. These follow the primary [Apple SDK](https://apple.github.io/app-store-server-library-node/classes/SignedDataVerifier.html),
2034
+ [push authentication](https://docs.cloud.google.com/pubsub/docs/authenticate-push-subscriptions)
2035
+ and [RTDN reference](https://developer.android.com/google/play/billing/rtdn-reference).
2036
+
2037
+ Both server-only `IapNotificationPort` services require initialize/verify.
2038
+ Verification returns opaque process-local evidence; `iapNotificationNotice`
2039
+ reads its redacted metadata. Copies/serialized references refuse. Raw purchase
2040
+ tokens and signed data remain private. Captured byte/time budgets are not
2041
+ delivery-capacity claims. No shared webhook secret, token query-string, player
2042
+ grant/state, SQL write or automatic financial adjustment is added.
2043
+
2044
+ Notification authentication **does not establish current financial state**.
2045
+ Fresh provider lookup, accepted SQL owner/custody, deduplication, ordering,
2046
+ durable receipts, renewals/refunds/debt and paid-asset disposition remain
2047
+ required construction work. The verifier alone installs no route; the explicit
2048
+ optional installed notification/refund binding is described below. Do not
2049
+ acknowledge provider delivery as durably processed using this verifier alone.
2050
+
2051
+ `./testing` exports fresh scoped `iapNotificationPortFixture`,
2052
+ `iapNotificationFakeLayer` and `iapNotificationPortSystemFixture`. Their
2053
+ explicit synthetic TEST authority has no purchase identity or grant/refund
2054
+ authority; normal scope release permanently revokes verify/initialize. Shared
2055
+ verifier and SDK mocks only measure seam configuration and refusal, not real
2056
+ cryptographic or delivery behavior.
2057
+
2058
+ ## Accepted SQL notification targets
2059
+
2060
+ `IapSqlService.prepareNotification(deployment, authenticatedNotice)` is a trusted
2061
+ server-only read helper. Capture the realMoney/channel/app/environment declaration
2062
+ from server configuration; never take it from the player or the push body. The
2063
+ notice must be the original process-local evidence returned by an authenticated
2064
+ notification adapter. The helper looks up the globally claimed transaction identity,
2065
+ checks its actual namespace, owner privacy and accepted SQL custody, and decodes the
2066
+ original immutable order snapshot and financial receipt. It performs no seeding,
2067
+ clock read, current verifier/admission callback, asset write or provider call.
2068
+
2069
+ ```ts
2070
+ import { observeAcceptedIapNotificationTarget } from '@voltro/plugin-economy/server'
2071
+
2072
+ const target = yield* orders.prepareNotification(deployment, authenticatedNotice)
2073
+ const original = yield* observeAcceptedIapNotificationTarget(target)
2074
+ ```
2075
+
2076
+ The opaque target exposes only orderId/noticeId. `observeAcceptedIapNotificationTarget`
2077
+ repeats the actual SQL/privacy reads before returning the original receipt and the
2078
+ redacted authenticated notice. Spread/JSON copies have no authority. Erasure,
2079
+ missing/corrupt custody, foreign namespace/app/SKU/original transaction and changed
2080
+ order ownership refuse; a saved target cannot bind another order. A nullable RTDN
2081
+ environment is resolved only through the captured declaration and a matching actual
2082
+ accepted SQL identity. That establishes SQL scope, never current Google state.
2083
+ TEST notices and review hints without a transaction identity cannot select a purchase.
2084
+
2085
+ This read-only capability authorizes neither a grant nor a refund. Current
2086
+ provider status must be looked up separately before a financial adjustment;
2087
+ provider I/O and SQL erasure are not one atomic transaction. No raw push
2088
+ material or owner identity is projected through this observation helper. No new
2089
+ tables or installed webhook route are added by this read helper alone; optional
2090
+ installed binding is described below.
2091
+
2092
+ ## Fresh IAP provider status
2093
+
2094
+ `IapCurrentStatePort`/`iapCurrentStatePort` is a server-only initialize/read seam.
2095
+ It requires the original opaque accepted SQL notification target and captured
2096
+ realMoney/app/channel/environment/account configuration. `IapAccountsPort` now
2097
+ also supplies `observe`: it reads and validates an existing SQL binding without
2098
+ minting, seeding or fence updates. Missing, corrupt or erased bindings refuse.
2099
+ This observation path, rather than the allocating `read`, gates the new status
2100
+ service. Original SQL and current account are rechecked after external I/O and on
2101
+ every subsequent use of the opaque status evidence.
2102
+
2103
+ ```ts
2104
+ import { iapCurrentPurchaseState } from '@voltro/plugin-economy/server'
2105
+
2106
+ const accepted = yield* orders.prepareNotification(deployment, authenticatedNotice)
2107
+ const verified = yield* currentProvider.read(accepted)
2108
+ const status = yield* iapCurrentPurchaseState(verified)
2109
+ ```
2110
+
2111
+ `googleIapCurrentStatePort` uses the same bounded, authenticated, fixed Products v2
2112
+ GET as the verifier/completion adapters. It reads the purchase token only from the
2113
+ private authenticated notice, checks its hash against accepted SQL, and compares
2114
+ actual account, app/environment, SKU, purchase time and quantity. Current
2115
+ refundableQuantity gives an exact cumulative share `(quantity - refundableQuantity)
2116
+ / quantity`; provider consumption/acknowledgement can advance. A missing/error,
2117
+ pending or inconsistent cancelled response never implies a full refund.
2118
+
2119
+ `appleIapCurrentStatePort` reads current operator credentials, calls the real SDK
2120
+ getTransactionInfo and verifies that response with captured public Apple roots,
2121
+ online checks and app/environment scope. Purchase identity/value/account admission
2122
+ is shared with the original verifier. Legacy date-only/full refund gives 1/1;
2123
+ REFUND_PRORATED uses exact integer milli-percentage / 100000. Contradictory fields,
2124
+ unsupported Family Sharing revoke and foreign/changed purchase facts refuse.
2125
+
2126
+ Opaque evidence exposes only orderId. `iapCurrentPurchaseState` returns redacted
2127
+ state, exact refundNumerator/refundDenominator, observedAt and optional revokedAt.
2128
+ Copying this projection does not restore evidence. maxAgeMs (default 30000) is an
2129
+ evidence lifetime budget; expiry or clock rewind refuses. It is not a provider
2130
+ consistency or latency guarantee. SQL/account erasure and provider I/O remain
2131
+ separate transactions; a future financial writer still needs its own SQL fence.
2132
+ No refund, debt, entitlement, provider mutation, new table or installed webhook
2133
+ route comes from this read service. notificationInstalled/refundDebtInstalled stay
2134
+ false; no raw token, signing key or owner identity reaches its public projection.
2135
+
2136
+ Twelve source scenarios cover existing-account observation, actual SQL/account
2137
+ revalidation, provider callsites/projection/transport refusal and scoped Tag
2138
+ release; one native independent-pool scenario per six variants is authored.
2139
+ Provider HTTP/SDK/authenticated-notice seams are explicit; no real purchase,
2140
+ push, signature/OCSP or sandbox delivery proof. `./testing` exports fresh
2141
+ iapCurrentStatePortFixture/iapCurrentStateFakeLayer/iapCurrentStatePortSystemFixture
2142
+ with actual accepted SQL and an explicit active-provider seam.
2143
+
2144
+ Provider field contracts: [Google Products v2](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.productsv2),
2145
+ [Apple transaction payload](https://apple.github.io/app-store-server-library-node/interfaces/JWSTransactionDecodedPayload.html)
2146
+ and [Apple revocation types](https://apple.github.io/app-store-server-library-node/enums/RevocationType.html).
2147
+
2148
+ ## Durable encrypted IAP notice custody
2149
+
2150
+ The explicit server-only `./iap-notifications` entry adds
2151
+ `IapNotificationCustodyPort`/`dataStoreIapNotificationCustody`. Its three table roles
2152
+ are notice namespace/count admission, subject privacy/count admission and immutable
2153
+ notice identities with pending/expired/erased custody. Importing the ordinary root
2154
+ or `./server` does not import these new table declarations. This is trusted worker
2155
+ custody, not a player route or a completed refund processor.
2156
+
2157
+ ```ts
2158
+ import { dataStoreIapNotificationCustody } from '@voltro/plugin-economy/iap-notifications'
2159
+
2160
+ const custody = dataStoreIapNotificationCustody(store, {
2161
+ namespace,
2162
+ deployment,
2163
+ orders,
2164
+ bodyRetentionMs,
2165
+ })
2166
+ yield* custody.initialize()
2167
+ const saved = yield* custody.retain(authenticatedNotice)
2168
+ const resumedNotice = yield* custody.restore(saved.id)
2169
+ const original = yield* orders.prepareNotification(deployment, resumedNotice)
2170
+ const current = yield* currentProvider.read(original)
2171
+ ```
2172
+
2173
+ Declare actual deployment app/channel/environment, a server-owned namespace and
2174
+ explicit bodyRetentionMs. Construction performs no SQL/crypto/provider I/O.
2175
+ initialize requires the registered framework FieldCipher; retain refuses missing
2176
+ cipher before notice admission/seeding and never stores cleartext. Explicit cipher
2177
+ sources may provide the same FieldCipher codec/row-binding contract; the default
2178
+ reads the registered cipher per operation so configured key rotation is observed.
2179
+ Keys and provider credentials come from the real deployment, never examples.
2180
+
2181
+ retain first resolves the authenticated notice to actual accepted IAP SQL/privacy.
2182
+ Its immutable identity is channel/environment/app/noticeId; changed content, order
2183
+ or owner conflicts. Namespace/subject fences serialize bounded whole-namespace
2184
+ counts and privacy. The body authenticates namespace/deployment/order/owner/times/
2185
+ notice/private material/fingerprint and is sealed to its exact table/column/row
2186
+ address using the existing framework codec, as in the connection vault. sealedBody
2187
+ is deliberately plain server-only text holding ciphertext; automatic field
2188
+ middleware must not encrypt/decrypt it a second time. Unbounded SQL text follows
2189
+ the existing LONGTEXT/NVARCHAR(MAX) mapping; plaintext and sealed-body work are
2190
+ separately bounded. There is no plaintext or unbound-envelope fallback.
2191
+
2192
+ restore requires actual retained SQL custody, row-bound authenticated decryption,
2193
+ matching metadata/body/fingerprint, current custody privacy and the original IAP
2194
+ SQL order again. It can issue a new process-local authenticated notice from that
2195
+ trusted encrypted SQL provenance after a process/pool restart. A public notice,
2196
+ serialized opaque object, copied ciphertext or changed metadata cannot do that.
2197
+ The restored notice still needs a fresh provider read before any financial action.
2198
+ Expiry is checked when the Effect runs and after original SQL revalidation.
2199
+ Restored evidence also carries an internal async SQL-custody validity check: every
2200
+ accepted-target use rechecks this exact encrypted row, privacy and expiry before
2201
+ entering the IAP transaction. Holding a restored notice does not bypass pruning
2202
+ or erasure. The two transactions remain separate, without a nested SQL lease.
2203
+
2204
+ pending returns bounded redacted receipt identities; it is not a processed-status
2205
+ or refund guarantee. Explicit prune removes expired private bodies, keeps identity
2206
+ and refuses payload recreation on redelivery. No automatic retention schedule is
2207
+ installed yet. Export returns owned metadata without raw tokens/ciphertext; erase
2208
+ removes owner/body and permanently fences recreation. Erase also works without a
2209
+ cipher or with corrupt ciphertext. Detected original IAP erasure during retain/
2210
+ restore triggers custody cleanup. These are separate SQL transactions; a future
2211
+ installed privacy/financial workflow must still coordinate its own fences. No
2212
+ cross-store/distributed atomicity is claimed.
2213
+
2214
+ One native scenario per six variants covers real cipher, independent-pool
2215
+ restore, a payload beyond 65535 bytes, expiry and cipher-free erase. This is not
2216
+ executed dialect evidence. ./testing exports
2217
+ iapNotificationCustodyPortFixture/iapNotificationCustodyFakeLayer/
2218
+ iapNotificationCustodyPortSystemFixture with fresh SQL and a privately owned
2219
+ real framework cipher; it does not replace the process-global key. The
2220
+ authenticated notice is an explicit test seam. The standalone custody port
2221
+ installs no default tables or route; the optional installed notification/refund
2222
+ binding below contributes those declarations. No provider delivery, load or
2223
+ support claim follows from this source.
2224
+
2225
+ ### Current IAP state in a SQL mutation group
2226
+
2227
+ `IapSqlService.prepareCurrentState(proof)` returns an actual-store-bound
2228
+ `PreparedSqlMutation<IapCurrentPurchaseState>` for trusted financial consumers.
2229
+ It repeats original accepted IAP custody, account binding and, for restored
2230
+ notices, encrypted notification custody/expiry inside the same SQL transaction.
2231
+ The account source must supply both `observe` and `prepareBinding`. A foreign
2232
+ store participant or serialized evidence refuses; there is no fallback.
2233
+
2234
+ Preparation reads the opaque provider observation once. Commit uses SQL and the
2235
+ captured server clock, without a provider request or nested SQL transaction.
2236
+ The original IAP fence precedes account and notification fences. Evidence
2237
+ lifetime is checked again when the group runs. Consumers must preserve this
2238
+ lock order and finish their financial group with their own freshness check.
2239
+ The participant itself grants or reverses no assets and cannot guarantee that
2240
+ an external provider has remained unchanged since its response.
2241
+
2242
+ Five authored memory-store scenarios cover same-store composition, post-prepare
2243
+ erasure, wallet rollback, time expiry, mixed-store refusal and encrypted-notice
2244
+ revocation/tampering. The native six-variant scenario also exercises account
2245
+ erasure from a second pool after preparation.
2246
+
2247
+ ### SQL IAP refunds, reversals and hard-currency debt
2248
+
2249
+ The server-only `@voltro/plugin-economy/iap-refunds` entry declares four optional
2250
+ refund roles and exposes `IapRefundPort` plus `dataStoreIapRefunds`. Its five
2251
+ methods are `reconcile`, `rebuild`, `observe`, `exportSubject` and `eraseSubject`.
2252
+ It is an explicit trusted server participant; the ordinary IAP installation
2253
+ does not yet install notification routes or refund workers. Do not expose
2254
+ `reconcile` as a player-authored status/amount mutation. It accepts only the
2255
+ original opaque `VerifiedIapCurrentState` produced from accepted SQL and fresh
2256
+ provider lookup. `realMoney:false` refuses this real-money component; virtual
2257
+ wallets and virtual shop capabilities remain separate.
2258
+
2259
+ Supply actual same-store `IapSqlService` and `EconomySqlService`, an explicit
2260
+ `debtPolicy: 'allowNegative' | 'blockSpend'`, a namespace and, for item orders,
2261
+ a same-store asset participant with a nondefault `assetsVersion`. There is one
2262
+ immutable refund cursor per accepted original across namespaces. Changing the
2263
+ namespace cannot authorize another refund of the same purchase. SQL adjustment
2264
+ history, owner fences, original IAP/account/retained-notice fences and balanced
2265
+ wallet/item movement receipts commit together. No provider I/O runs in the
2266
+ financial transaction. Whole-history/SQL-unit bounds refuse excess work.
2267
+
2268
+ Currency reversal uses exact cumulative `floor(original * numerator /
2269
+ denominator)`. Each adjustment books only the difference from the preceding
2270
+ accepted cumulative amount. Older observed states return the newer SQL receipt;
2271
+ contradictory states at the same observation time refuse. A later fresh active
2272
+ observation compensates an earlier refund through the original source account.
2273
+ Repeated accepted observations cannot book the same adjustment again. `rebuild`
2274
+ repairs only the refund projection from complete accepted adjustment history and
2275
+ actual balanced wallet/item evidence, without replaying grants.
2276
+
2277
+ `EconomySqlService.prepareIapRefund` uses the existing wallet writer and requires
2278
+ the opaque current-state proof and original accepted wallet receipt. Only
2279
+ hard-currency refund debits can create debt. Ordinary credits repay signed debt;
2280
+ ordinary debits cannot create or deepen it. `allowNegative` permits otherwise
2281
+ funded spending in other currencies; `blockSpend` refuses all wallet debits for
2282
+ that owner while a wallet has blocked debt. Existing blocked debt cannot be
2283
+ weakened by a later refund policy. Nullable wallet policy and owner sequence/policy
2284
+ on SQL ledger movements preserve the policy for wallet rebuild. The signed
2285
+ financial ledger remains the source of the balance, with exact BIGINT overflow
2286
+ and positive cap checks. Soft-currency debits remain nonnegative.
2287
+
2288
+ `inventoryIapRefundAssets` reuses actual `InventorySqlService.prepareConsume` and
2289
+ `prepareGrant`. The studio must explicitly choose `disposition: 'requireAll' |
2290
+ 'recoverRemaining'`. `requireAll` refuses the entire financial group when the
2291
+ requested items cannot be recovered. `recoverRemaining` removes only remaining
2292
+ owned eligible quantities and durably reports unrecoverable consumed, moved or
2293
+ locked quantities. It never pursues another owner or counts missing items as
2294
+ removed. A refund reversal restores only quantities actually recovered, with
2295
+ original attributes through current inventory admission, and tracks actual
2296
+ restored instance receipts for a later refund. The adapter does not invent an
2297
+ expired-item restitution policy or bypass changed catalog rules. A custom asset
2298
+ participant supplies prepared same-store mutations, pure receipt admission and
2299
+ SQL-only audit of its accepted movement history.
2300
+
2301
+ Export returns owned reconciliation metadata. Erasure removes receipt bodies and
2302
+ ownership, preserves immutable identities and fences recreation; it does not
2303
+ silently cancel financial postings. The separate original IAP, account, wallet,
2304
+ inventory and notification privacy hooks must also be wired into the application
2305
+ cascade. Notification processing, installed refund/debt inspect wiring, refund
2306
+ misuse events/sanctions and store subscriptions/entitlements remain unfinished.
2307
+
2308
+ Seven actual memory-SQL scenarios, two scoped Tag/lifetime sources and a case
2309
+ for each of six native variants have been authored, including fractional
2310
+ rounding, replay, reversal, restored-instance recovery, cross-namespace refusal,
2311
+ debt repayment/rebuild, explicit item disposition, late privacy revocation,
2312
+ expiry, bounds and cross-pool reads. They have not run; provider/notification
2313
+ projections in these fixtures are explicit seams.
2314
+
2315
+ ### Notification processor, SQL completion and explicit ingress
2316
+
2317
+ The `./iap-notifications` server entry also exposes the seven-method
2318
+ `IapNotificationProcessorPort`: `initialize`, `ingest`, `process`, `pending`,
2319
+ `prune`, `exportSubject` and `eraseSubject`. `iapNotificationProcessor` captures
2320
+ actual notification, current-state, custody, original-order and refund ports.
2321
+ It authenticates delivery, retains the private body under the framework cipher,
2322
+ restores SQL provenance, reads fresh provider state, executes the financial
2323
+ reconciliation group and marks SQL custody `processed`. Failed provider or
2324
+ financial work leaves pending SQL for explicit retry.
2325
+
2326
+ Custody now has eight methods, adding `markProcessed(id, receipt)`. It accepts
2327
+ only the process-local capability attached to a successful actual SQL refund
2328
+ result. A spread copy or JSON projection cannot authorize the marker. The marker
2329
+ joins fresh original/account/retained-notice and actual accepted refund-journal
2330
+ fences on the same store before changing custody. It is a second SQL commit
2331
+ after the financial group; a lost marker reply retries accepted history, without
2332
+ repeating financial value. Duplicate authenticated processed delivery avoids
2333
+ another provider call. Retention removes the processed private body while
2334
+ preserving the immutable completion marker. Owned erasure removes bodies and
2335
+ refund ownership through both ports. This is not an atomic privacy cascade over
2336
+ all application systems.
2337
+
2338
+ `iapNotificationHttpRoute({ path, maxBodyBytes, processor })` returns a normal
2339
+ exact POST plugin HTTP declaration. Register it in `httpRoutes`; the helper does
2340
+ not start a listener. Provider signatures or push bearer authentication supply
2341
+ authority; browser cookies and query parameters do not. It applies the explicit
2342
+ byte budget, fatal UTF-8 decoding, precise method refusal, no-store responses
2343
+ and redacted authentication/retry errors. A success returns 204 only after the
2344
+ processor completes. This declared route is origin-exempt because its concrete
2345
+ notification verifier requires provider authority independent of cookies. A full
2346
+ processor replacement must preserve that authentication contract.
2347
+
2348
+ `runIapNotificationWork({ processor, maxNotices })` prunes retained bodies and
2349
+ processes the whole bounded pending set. Excess refuses without accepting a
2350
+ prefix. Earlier successful notices survive a later firing failure and a retry
2351
+ reads SQL again. `iapNotificationSchedule({ name, cron, timezone, processor,
2352
+ maxNotices })` returns an ordinary discovered schedule descriptor. Cadence,
2353
+ registration and body retention are operator-owned; no timer or worker is
2354
+ started by construction.
2355
+
2356
+ They include pending retry, copied-financial-capability refusal, late account
2357
+ erasure, processed-body expiry, SQL marker replay and malformed HTTP admission.
2358
+ Handler invocation is not normal gateway discovery/startup; synthetic provider
2359
+ and authentication fixtures are not genuine delivery/signature evidence.
2360
+ Optional IAP plugin binding and four-layer configuration panels are described
2361
+ below.
2362
+
2363
+ ### Optional installed notification and refund system
2364
+
2365
+ `IapPluginOptions.notifications` now installs the actual notification/refund
2366
+ chain when explicitly configured. Its `version` must be nondefault. The SQL
2367
+ default requires operator-selected `bodyRetentionMs` and `debtPolicy`:
2368
+ `allowNegative` keeps hard-currency refund debt while permitting funded spending
2369
+ in other currencies; `blockSpend` blocks owner wallet debits until debt is repaid.
2370
+ Ordinary credits repay debt. No retention, pricing or region decision is guessed.
2371
+ Item orders additionally require an actual prepared `assets` participant and
2372
+ nondefault `assetsVersion`; `inventoryIapRefundAssets` supplies the described
2373
+ SQL recovery participant over the app's actual Inventory adapter. Currency-only
2374
+ orders may omit it. The same original/account/notice/current-state/refund/wallet/
2375
+ item SQL authorities described above govern every financial adjustment.
2376
+
2377
+ The binding requires both `createNotifications` and `createCurrentState` on each
2378
+ configured provider. Only iOS and Android have the default chain. Apple provider
2379
+ construction shares the captured trust configuration with purchase verification
2380
+ and fresh-state lookup. Google requires explicit authenticated push configuration
2381
+ in `googleIapProvider({ ...purchaseOptions, notifications: { audience,
2382
+ serviceAccountEmail, subscription } })`; these three identities are app-owned,
2383
+ not derived from the package name. Missing factories refuse notification
2384
+ installation before credential access. Without `notifications`, purchase-only
2385
+ installations construct neither factory. Every initializer is awaited before
2386
+ IAP activation; missing required credentials or framework cipher refuse readiness.
2387
+ Declared IAP systems carry `network:outbound:*` permission for their explicitly
2388
+ supplied provider authorities, like Billing. Virtual-only installations acquire
2389
+ no IAP outbound permission or routes and refuse real-money declarations first.
2390
+
2391
+ The normal plugin declaration adds exact POST paths `/_voltro/economy/iap/ios`
2392
+ and/or `/_voltro/economy/iap/android`. `pathPrefix` may select one safe common
2393
+ prefix; `maxBodyBytes` is the explicit ingress work budget, default 262144 bytes.
2394
+ Default `maxNoticeRows` and `maxRefundRows` are 10000-row work budgets, never
2395
+ capacity results. SQL custody is isolated per provider deployment; one shared
2396
+ refund journal keeps global original-order idempotence. No raw custody or refund
2397
+ Tag is ambiguously installed for multiple deployments.
2398
+
2399
+ `economy.economy.iapNotifications()` and the server-only
2400
+ `IapNotificationsSystemPort` expose `initialize`, `processor(channel)`,
2401
+ `exportSubject` and `eraseSubject`. Resolve a channel-selected seven-method
2402
+ processor only after activation. Escaped processors and pending old-generation
2403
+ outcomes refuse after deactivation or rebinding. Explicit same-store rebinding
2404
+ reconstructs services from SQL without regranting assets. Initialization never
2405
+ starts its own timer, listener or worker.
2406
+
2407
+ For ordinary file discovery, use this in an app-owned `*.cron.tsx` file. The
2408
+ cadence and budget below are examples, not an operator retention or deployment
2409
+ promise. `economy` is the actual configured plugin instance exported by that app.
2410
+
2411
+ ```ts
2412
+ import { installedIapNotificationSchedule } from '@voltro/plugin-economy/iap-notifications'
2413
+ import { economy } from '../plugins/economy'
2414
+
2415
+ export default installedIapNotificationSchedule({
2416
+ system: economy.economy.iapNotifications(),
2417
+ channel: 'ios',
2418
+ name: 'iap.notifications.ios',
2419
+ cron: '*/5 * * * *',
2420
+ timezone: 'UTC',
2421
+ maxNotices: 1000,
2422
+ })
2423
+ ```
2424
+
2425
+ The descriptor captures the facade at discovery and resolves the selected
2426
+ processor at firing, so discovery needs neither activation nor credentials.
2427
+ Register one schedule per configured deployment. Pruning and pending retry use
2428
+ the existing bounded processor work; there is no automatic background runtime.
2429
+
2430
+ A complete four-method `IapNotificationsSystemService` replacement supplies
2431
+ `service` and an explicit version, owns provider authentication/current-state/
2432
+ finance/queue/retention/privacy, and forbids default SQL/cipher/debt/assets/bounds
2433
+ options. The plugin still enforces exact HTTP paths/body budget and lifetime.
2434
+ A complete IAP replacement requires this complete notification replacement;
2435
+ default SQL notifications never borrow a replaced IAP or Wallet. Private class
2436
+ receivers are captured. Default construction adds seven notification/refund
2437
+ roles; replacement adds none. Parent initialization/export/erase include the
2438
+ selected notification system. Default erase revokes accounts first, then notice
2439
+ and refund bodies, then original IAP and root Wallet privacy. Inventory retains
2440
+ its own installed privacy hook. Partial privacy work is retryable, not atomic
2441
+ across systems.
2442
+
2443
+ Server inspect, Shared UI, Local transport and Cloud schema/proxy now require the
2444
+ same exact `notificationSystem` shape, conditional seven roles, provider channels/
2445
+ paths, binding/version, debt/item policies and work budgets. No private token,
2446
+ notice body, owner, balance or refund outcome is exposed. Optional configuration
2447
+ sets `notificationInstalled` and `refundDebtInstalled`; absent configuration
2448
+ requires false flags and null status. Replacement shows app-owned policy/bounds.
2449
+ Both EN/DE panels retain the existing refresh/error/focus behavior. This is
2450
+ configuration, not provider readiness, authenticated delivery or financial success.
2451
+
2452
+ `./testing` additionally exports `iapNotificationsSystemPortFixture`,
2453
+ `iapNotificationsSystemFakeLayer` and `iapNotificationsSystemPortSystemFixture`.
2454
+ Each resolves the actual installed SystemPort and processes a real SQL refund
2455
+ with a privately owned framework cipher. Provider authentication/current-state
2456
+ are explicit synthetic seams. Scope release uses normal plugin deactivation; all
2457
+ four outer methods and escaped processors become unavailable.
2458
+
2459
+ ### Accepted refund events and explicit sanctions classification
2460
+
2461
+ Every accepted refund/reversal journal entry now commits `eventState: pending`
2462
+ and null `eventDeliveryVersion` in the same financial transaction. These two
2463
+ server-only columns belong to the existing adjustment table; no second journal
2464
+ or new table role is created. A financial rollback creates neither value nor a
2465
+ pending event. Event acknowledgement only changes delivery metadata, never
2466
+ accepted amounts, asset movements or financial bodies. Refund projection rebuild
2467
+ does not replay or recreate delivered events.
2468
+
2469
+ `dataStoreIapRefunds` returns `IapRefundSqlService`, which includes its actual
2470
+ three-method `events` port: `pending(subject)`, `read(subject, opaqueEvent)` and
2471
+ `acknowledge(subject, opaqueEvent, deliveryVersion)`. Only system Subjects or
2472
+ verified tenant service accounts with `economy:events:write` may consume it.
2473
+ The complete bounded journal and actual accepted order/financial history are
2474
+ checked under refund namespace/subject fences. A pending object carries only its
2475
+ event ID; copies, JSON and objects from another service cannot authorize reads
2476
+ or acknowledgement. The same actual capability/version acknowledgement is
2477
+ idempotent; a changed version refuses. Delivered entries do not reopen. Refund
2478
+ erasure removes owners/bodies and delivery versions, marks event custody erased
2479
+ and revokes saved pending authority. No raw receipt/token or expired encrypted
2480
+ notification is needed to resume the already accepted financial journal.
2481
+
2482
+ The installed default notification system now resolves this one shared queue
2483
+ through `IapRefundEventsPort` or `economy.economy.iapRefundEvents()`. It is
2484
+ lifetime- guarded. Complete notification replacements may supply their own
2485
+ `refundEvents` port; that option is forbidden for the SQL default. An absent
2486
+ replacement queue contributes no Tag and returns unavailable, never default SQL.
2487
+ The replacement owns its queue/privacy contract. All four inspect layers now
2488
+ require `eventQueue: sql | custom | null` and both panels show configured queue
2489
+ ownership and the need for an explicit delivery schedule. They display no
2490
+ pending owner/event/outcome enumeration. Optional descriptor discovery ships the
2491
+ browser- safe `./events` declarations `economy.refunded` and
2492
+ `economy.refundReversed` only when an event queue is configured; the production
2493
+ build copies that source tree. Descriptor guards require `economy:events:read`.
2494
+
2495
+ `deliverIapRefundEvents({ events, publisher, version, maxEvents, sanctions,
2496
+ abusePolicy })` reads the whole pending set. `maxEvents` defaults to a 1000-row
2497
+ work budget; excess refuses before any prefix delivery. `publisher.read(tenantId)`
2498
+ must supply the actual tenant-bound framework `EventPublishContext`, including
2499
+ its configured durable workflow audience. Foreign/null tenants and deferred
2500
+ transaction contexts refuse. The helper uses the existing `publishEvent`, with
2501
+ its real key/payload admission and size guard. Publication happens outside the
2502
+ financial SQL group, and acknowledgement follows successful nondeferred
2503
+ publication. A lost reply between publication and acknowledgement can duplicate
2504
+ an ephemeral event; consumers must deduplicate its stable event ID. SQL pending
2505
+ custody survives process loss. A successful acknowledgement says the framework
2506
+ publisher accepted delivery, not that a workflow finished or a client received
2507
+ it. Without a durable audience, the EventBus retains its ordinary ephemeral
2508
+ semantics. `iapRefundEventSchedule` returns a normal app-discovered descriptor;
2509
+ no timer, event bus, tenant, listener or worker is invented or started.
2510
+
2511
+ The exported pure default `noRefundAbuseSignal` returns null. An explicit
2512
+ `abusePolicy` over actual SQL event context may return a bounded confidence
2513
+ value; it requires an actual supplied `SanctionsService.recordSignal`. The
2514
+ context reports bounded observed originals, currently refunded originals and
2515
+ refund adjustments, not all purchases or an invented refund rate. Classified
2516
+ positive refund states become trusted `rmt` signals keyed to the original order;
2517
+ replay/partial updates cannot increment a second strike for that same accepted
2518
+ original and identical classification. Changed classification conflicts.
2519
+ Reversals do not run the classifier. The Sanctions default keeps `rmt`
2520
+ review-only; automatic measures require its explicit escalation strategy.
2521
+ Missing or failing installed consumers leave SQL pending, do not repeat
2522
+ financial value and are not silently bypassed. External
2523
+ publication/classification and erasure are not one distributed atomic
2524
+ transaction.
2525
+
2526
+ Installed Tag/lifetime sources and strict Shared/Cloud/Local/ ENDE DOM sources
2527
+ are extended. `./testing` exports `iapRefundEventsPortFixture`,
2528
+ `iapRefundEventsFakeLayer` and `iapRefundEventsPortSystemFixture`; release
2529
+ revokes actual account/journal authority plus the facade. Whole-journal cost is
2530
+ bounded by configuration but not measured. Full entitlement, sanctions workflow/
2531
+ GM/template/engine integration and remaining monetization stay open.
2532
+
2533
+
2534
+ ### Own IAP refund disposition
2535
+
2536
+ `IapPort.receipt(subject, originalHashReference)` and `useIapReceipt` now expose
2537
+ `partially-refunded` and `refunded` when a configured disposition reader admits
2538
+ accepted SQL refund history. Both contain the unchanged original `receipt` plus
2539
+ `refund: { sequence, state, refundNumerator, refundDenominator, observedAt }`.
2540
+ Missing and pending originals keep their existing nonterminal states. No private
2541
+ notice, provider token, fingerprint, owner or refund movement is returned.
2542
+
2543
+ The default installed notification system supplies the actual shared refund
2544
+ adapter. The original order, retained purchase custody, refund namespace/subject,
2545
+ complete bounded adjustment history, original wallet transfers and configured
2546
+ item movement participant are read in one borrowed SQL transaction. The read
2547
+ seeds no rows, updates no admission fences, opens no nested transaction, calls no
2548
+ provider and evaluates no clock or current catalog. Invalid, erased, incomplete,
2549
+ over-budget or inconsistent history refuses rather than displaying `fulfilled`.
2550
+ The original purchase remains immutable SQL truth; this projection grants nothing
2551
+ and is not a provider observation or store-completion authority. A budget is not
2552
+ measured capacity.
2553
+
2554
+ Standalone `dataStoreIap` accepts an explicit `refundDisposition: { read }` SQL
2555
+ reader. `dataStoreIapRefunds(...).readDisposition` implements this contract using
2556
+ the caller's transaction and admitted `IapSqlOriginalPurchase`; it cannot refund,
2557
+ repair or grant. Asset audit receives that admitted original purchase without
2558
+ requiring a retained notification body. Default installed queries conditionally
2559
+ include all four refund table dependencies for LiveQuery invalidation; absent
2560
+ notification configuration adds none.
2561
+
2562
+ A complete notification replacement over default SQL IAP may supply its own
2563
+ `refundDisposition` reader and must declare actual nonempty IAP
2564
+ `querySources.receipt` dependencies. The reader preserves its receiver and must
2565
+ obey the borrowed-transaction read contract. No default refund adapter is loaned
2566
+ into that replacement. Omission reports no disposition reader and exposes only
2567
+ the original order. A complete IAP replacement owns its own `receipt` contract
2568
+ and cannot configure this default-reader seam. The four inspect layers require
2569
+ `notificationSystem.orderDisposition: sql | custom | null` and show configuration
2570
+ ownership only, never an order's refund outcome.
2571
+
2572
+ `captureIapObservation` shares strict admission between the installed facade,
2573
+ wire schema and recovery. Financial `partially-refunded`/`refunded` observations
2574
+ retain an uncertain completion journal; they cannot replace verified store
2575
+ completion. Explicit recovery reuses the accepted original and never redeems it
2576
+ again. A failed completion keeps the original hash reference for an explicit
2577
+ retry. Client finishing may still fail for a revoked store transaction; no success
2578
+ or completion is inferred from its financial disposition.
2579
+
2580
+ This section makes no support claim.
2581
+
2582
+
2583
+ ### Accepted store subscription periods
2584
+
2585
+ `dataStoreIap` can now fulfill a verified `subscription` transaction through the
2586
+ same actual account/eligibility/Wallet/Inventory/order/custody prepared group.
2587
+ Quantity is exactly one and consumption must be `none`; `expiresAt` must exceed
2588
+ `purchasedAt` and acceptance time. A later distinct verified transaction can
2589
+ fulfill its own original once. Replaying an accepted original returns its receipt
2590
+ before current verification or clock and never grants another period. Existing
2591
+ logical-offer limits continue to apply. No timer, automatic renewal or purchase
2592
+ consent is manufactured.
2593
+
2594
+ The server-only extended SQL adapter adds `subscriptionPeriods(subject)`. It
2595
+ reads complete bounded own order/custody history and returns only
2596
+ `orderId`, `offerId`, `offerVersion`, `channel`, `startsAt`, `expiresAt` and accepted
2597
+ `refund` disposition. It does not seed, mutate, open a nested transaction, evaluate
2598
+ a clock or call a provider. A real configured refund disposition reader is
2599
+ required when an accepted subscription exists; missing admission refuses instead
2600
+ of asserting an unrefunded entitlement. Historical receipts remain readable after
2601
+ current catalog removal. Owner erasure, changed custody or corrupt history
2602
+ refuses. Structural IAP erasure no longer decodes private accepted payloads before
2603
+ redaction; valid bounded owner/custody linkage remains required and financial
2604
+ values are retained.
2605
+
2606
+ `./iap-entitlements` is browser-safe and exports `IapEntitlementsPort`, its
2607
+ four-method service capture, `defaultIapEntitlementPolicy` and
2608
+ `resolveIapEntitlement`. `./server` exports `iapEntitlementsPort` with explicit
2609
+ `realMoney: true`, version, actual order source, optional pure policy and server
2610
+ clock. A fresh clock is evaluated after the actual SQL source resolves. Partial
2611
+ refunds remain eligible by default; a named pure policy may narrow that
2612
+ decision, including refusing partial refunds, but cannot restore a fully
2613
+ refunded, future or expired period. Duplicate order projections and nonboolean
2614
+ decisions refuse. `entitled(subject, offerId)` returns `{ offerId, entitled,
2615
+ expiresAt, observedAt }`; `expiresAt` is null when denied. The service delegates
2616
+ export/erase to the captured order owner. It creates no SQL role, listener or
2617
+ background schedule. `realMoney: false` refuses before order binding; existing
2618
+ virtual Subscription services remain usable independently.
2619
+
2620
+ This is an observation of accepted SQL periods, not a fresh provider status or
2621
+ prepared value-write capability. No grace, billing retry, automatic renewal,
2622
+ upgrade/proration or unseen provider period is inferred. An application needs its
2623
+ actual provider renewal delivery before a later transaction appears in SQL.
2624
+ Apple's documented transaction `expiresDate` is static per transaction; a renewal
2625
+ creates another transaction with a new expiry. See [Apple expiresDate](https://developer.apple.com/documentation/appstoreserverapi/expiresdate)
2626
+ and [originalTransactionId](https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid).
2627
+
2628
+ Deliberately supplied completion authorities may now accept a subscription's
2629
+ actual original SQL capability. That does not install a native finish callback:
2630
+ StoreKit finish remains client-owned, and the current Google Products-v2 adapter
2631
+ still refuses subscriptions. Installed default admission and entitlement routes
2632
+ are described below; the configured flag is conditional.
2633
+
2634
+ Provider projection/completion fixtures are deliberate seams, never crypto,
2635
+ StoreKit or sandbox evidence. No support or capacity claim.
2636
+
2637
+ ### Installed store subscription entitlements
2638
+
2639
+ Optional `iap.entitlements: { version, policy? }` now binds the default accepted
2640
+ SQL-period port. A subscription product in the default installed IAP system
2641
+ requires this configuration. Default entitlements require the actual installed
2642
+ refund disposition reader; an absent reader refuses construction. Explicit
2643
+ nondefault rule identity is required. The named pure policy may only narrow
2644
+ accepted, unexpired, non-fully-refunded periods. Configuring this port does not
2645
+ make an incompatible provider verifier accept subscriptions. The Google adapter
2646
+ now selects its separate Subscription-v2/Orders path described below.
2647
+
2648
+ `economy.economy.iapEntitlements()` and `IapEntitlementsPort` resolve the same
2649
+ lifecycle-guarded four-method service. Owning activation initializes it before the
2650
+ parent becomes active; deactivation and generation changes revoke late reads.
2651
+ Default history shares the existing IAP/refund SQL roles and owning parent privacy.
2652
+ A complete `iap.entitlements.service` replacement owns expiry/history/privacy,
2653
+ requires actual nonempty `querySources`, captures class receivers, forbids a
2654
+ borrowed default policy and cascades its own export/erase. Full IAP replacement
2655
+ cannot borrow the default SQL entitlement implementation.
2656
+
2657
+ The optional `economy.iap.entitled` query and `useIapEntitlement` accept only
2658
+ `{ offerId }` for the verified own user. Actual original/custody/financial/refund
2659
+ and explicit app dependencies invalidate the default query. A complete replacement
2660
+ uses only its declared sources. Mere passage of time is not a database change;
2661
+ `economy.iap.checkEntitlement` and `useCheckIapEntitlement` explicitly obtain a
2662
+ fresh server observation, including after `expiresAt`. The application must check
2663
+ again when it needs freshness. Privileged operations must evaluate their actual
2664
+ server authority; a cached client boolean is not permission to grant value.
2665
+
2666
+ The four configuration layers report conditional
2667
+ `subscriptionEntitlementsInstalled` and exact `entitlementsSystem` backend/version/
2668
+ policy/SQL-history bound or app ownership. They never expose player periods,
2669
+ refund results or readiness. Failed reads hide stale configuration and retain
2670
+ reload focus. No configured source means no entitlement Tag, routes or RPC imports.
2671
+ Virtual/free Subscription systems remain independent under `realMoney: false`.
2672
+
2673
+ ### Google subscription originals and pinned financial status
2674
+
2675
+ `googleIapProvider`, `googleIapVerificationPort`, `googleIapCompletionPort` and
2676
+ `googleIapCurrentStatePort` share bounded authenticated transport. Subscription
2677
+ configuration requires `subscriptionPlans`, each naming `offerId`, `offerVersion`,
2678
+ `basePlanId` and `storeOfferId` (explicit null for no store offer). Both purchase
2679
+ and financial order must match that mapping and the real account/app/environment.
2680
+ No missing plan, out-of-app linking, replacement, add-on or proration fallback.
2681
+
2682
+ The verifier reads Subscription-v2 and its latest successful Orders record. Each
2683
+ paid original has the actual order ID as transaction identity; the token hash
2684
+ identifies its chain. Exact single-line paid Money is recorded at nanounit scale,
2685
+ never from reference prices. Only a processed, unrefunded, current accepted period
2686
+ can grant; server clock/consent/prepared value admission still applies. A new paid
2687
+ order can fulfill once under a new original request. Reusing the token cannot
2688
+ reuse a previously accepted order as new value. This adds no automatic renewal
2689
+ worker or native client journal.
2690
+
2691
+ Completion revalidates the accepted SQL original and reads that pinned financial
2692
+ order even after a later renewal. Subscription acknowledgement uses the official
2693
+ endpoint, then fresh authenticated state; an uncertain POST is never a completion
2694
+ receipt by itself. Current financial reads likewise pin the original order.
2695
+ Subscription cancellation/expiry is not a monetary refund. Full refund state
2696
+ revokes; processed partial refund totals reduce to an exact bounded fraction,
2697
+ with no rounding. Unrepresentable fractions refuse. Ordinary payment, privacy,
2698
+ refund debt and item writers remain the existing SQL paths.
2699
+
2700
+ RTDN subscription messages without an order ID now retain a chain hash and null
2701
+ transaction identity; the financial processor refuses to choose an original from
2702
+ that hint. A subscription Void RTDN carries the exact order ID and chain hash,
2703
+ then the fresh lookup authorizes any adjustment. One-time token identities remain
2704
+ unchanged. Automatic renewal routing, missed-period ingestion, account linking,
2705
+ grace/deferral/plan changes and historical provider expiry extensions remain open.
2706
+ Paid period snapshots are used conservatively with current provider admission;
2707
+ the SQL entitlement is not a claim of all current provider access.
2708
+
2709
+ See
2710
+ [Subscription-v2](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptionsv2),
2711
+ [Orders](https://developers.google.com/android-publisher/api-ref/rest/v3/orders),
2712
+ [acknowledgement](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptions/acknowledge)
2713
+ and [RTDN](https://developer.android.com/google/play/billing/rtdn-reference).
2714
+ Generated OAuth/fetch/notice seams prove no Play purchase, cryptography, Pub/Sub
2715
+ delivery or sandbox.
2716
+
2717
+
2718
+ ## Production license admission
2719
+
2720
+ Declare the actual `licensingPlugin()` before this plugin in the API app. Production and staging activation require its verified active or grace deployment-wide license to grant `economy`. Explicit development/test mode stays free without recurring per-instance notices; CLI owns the actual template first-boot receipt. Tenant-scoped licenses refuse until actual tenant-bound authority exists. This server-only gate preserves normal schema, store binding and cleanup; it runs before the original activation hook, not before schema/store binding.