Rising Tide Developers
Reference

Changelog

Version history for the Rising Tide Protocol API and SDK.

Notable changes to the protocol API and SDK.

sdk 0.14.165 + ui 0.7.194 (2026-10-17) - one wallet call per sign-in, and a payee may have no name

Cut mid-week inside pull request 490 and pinned by the six rt- apps the same day. Client apps move at the next weekly release, and keep one wallet call per page load until then.

sdk: Payment.recipientName may be null

Changed: Payment.recipientName?: string | null. A payment leg that pays a person carries their display name, or null when they have no account, a deleted one or a blank name, and the API returns null for those legs. The SDK's Zod schema already allowed it; the type now says so. Show the recipientAddress when the name is null. CurrencyType.id is documented as the number contracts use for the currency: STX 1, sBTC 2.

ui: the wallet is asked for once per sign-in

Changed: PrivyStateSync remembers the wallet rt-api's create-wallet returned, in localStorage under rt-privy-wallet:<Privy user id>, and a later page load uses it while the person's own Privy sign-in still lists that wallet. It no longer calls create-wallet on every mount. Signing out forgets it, and anything it cannot confirm (another user's entry, a wallet no longer listed, storage that is blocked) asks create-wallet as before. The address a person is shown as is still the one rt-api named, never one picked from Privy's list.

sdk 0.14.164 + ui 0.7.193 (2026-10-17) - a bomb may be priced at 0

Cut by release 2026-10-17, and every app pins it. Tic-Tac-Boom uses it to open lobbies whose bombs cost only gas.

sdk: use-bomb at amount 0

Changed: a game-action of use-bomb accepts amount: 0 on both paths, for a lobby whose bomb is priced at 0% of the buy-in. Such a bomb moves no money: on the connected-wallet path buildGameActionTransaction builds it in deny mode with an empty post-condition list, so nothing may leave the signer, and the Privy path sends the amount to /api/privy/sign/game-action, which signs the same plan. A negative or non-numeric amount is still refused. A join is unchanged: a seat always costs more than 0.

rt-api accepts the same range in the same change: the bomb's priceOfBuyInPct is 0 to 100 on POST /api/games, the contract-generate route and the smart-lobby recipe.

ui

No change of its own; it pins sdk 0.14.164.

sdk 0.14.163 + ui 0.7.192 (2026-10-05) - the pocket is a delegate wallet

Cut mid-week inside pull request 274 and pinned by the six rt- apps the same day. Client apps move at the next weekly release; none of them calls the renamed service.

The second wallet a person names in rt-delegates is a delegate wallet everywhere, code included. rt-api renamed its table, routes and event in the same change, so 0.14.162's rtClient.pocket calls routes that no longer exist.

sdk: rtClient.delegateWallet

Renamed: rtClient.pocket is rtClient.delegateWallet, and PocketService is DelegateWalletService. The methods are unchanged (get, open, fundStx, fundSats, empty, resign); they call /api/privy/delegate-wallet and /api/privy/sign/*-delegate-wallet.

Renamed: the types Pocket, PocketStatus, PocketBalances, PocketWithBalances, PocketReadResponse and the four write responses are DelegateWallet*, and each response's pocket field is delegateWallet.

Renamed: ProtocolEventType value POCKET_CREATED is DELEGATE_WALLET_CREATED. rt-api renamed the stored value, so events already written read the new name.

ui

No change of its own; it pins sdk 0.14.163.

sdk 0.14.162 + ui 0.7.191 (2026-10-05) - a pocket, and products that act for the person

Cut mid-week inside pull request 266 and pinned by every app the same day.

Lobbies and brackets opened before this release are retired. Their contracts take the old arguments, so the apps on this release cannot join, enter or play them. Pages, letters, bottles and tickets keep their arguments and are unaffected.

Every product contract now asks the protocol's rt-delegates registry whether the sender may act for the person a call names. A person's own wallet always may, and so may their pocket, a second wallet they own and name in the registry. The pocket pays; whatever it buys belongs to the person.

sdk: rtClient.pocket

Added: PocketService, as rtClient.pocket, on the person's own Privy session: get, open, fundStx, fundSats, empty and resign. Every method moves money only between the person's two wallets. rt-api resolves both addresses from the person's own rows, so no method takes an address, and an amount travels as a whole decimal string.

Added: ProtocolEventType values DELEGATE_SET, DELEGATE_REMOVED and POCKET_CREATED, and ActorKind value ASSISTANT, which rt-api writes.

sdk: bracket and game transactions name the person

Changed: buildSubmitBracketTransaction and buildGameActionTransaction pass the signer as the person a bracket entry, a buy-in, leaving a seat, claiming a draw and using a bomb are for. They match the templates rt-api generates from this version on. A lobby or bracket deployed before it takes the old arguments, so an app on these builders cannot act on one.

ui: the displayed address is the stored wallet

Fixed: PrivyStateSync shows the wallet rt-api stores for the person, not the first Stacks wallet in Privy's list. Once a person has a pocket they hold two, and the list order is not theirs to choose.

sdk 0.14.161 + ui 0.7.190 (2026-10-04) - products deploy at Clarity 6

Cut mid-week inside pull request 252, a day after release 2026-10-03, and pinned by every app the same day.

The product contract templates are written for Clarity 6, the default of epoch 4.0 that testnet and mainnet run. Clarity 4 removed as-contract, so every payout in the templates is now as-contract? with an allowance naming exactly the asset and amount it sends, and a contract's own principal is current-contract. A deploy pinned below Clarity 6 cannot compile them.

sdk: PRODUCT_CLARITY_VERSION

Added: PRODUCT_CLARITY_VERSION (6), the Clarity version every product contract deploys at. A client that deploys a product from a person's own wallet sends it as clarityVersion. It mirrors the version rt-api's custodial deploy route sends.

ui: useDeployProduct

Changed: a deploy from a person's own wallet sends PRODUCT_CLARITY_VERSION, replacing a hardcoded Clarity 2. An app on ui 0.7.189 or earlier cannot deploy a product from a person's own wallet once rt-api serves the Clarity 6 templates; deploys signed through rt-api are unaffected.

ui 0.7.189 (2026-10-03) - the hub installs as a glyph

The ui tier release 2026-10-03 cuts, on sdk 0.14.160 (unchanged). Every app that carries the tarballs moves to it with this release. It carries 0.7.188's push change below.

ui: UserAppRail

Changed: the hub's rail glyph is House, replacing User. The install-icon family draws each protocol app's rail glyph on the stone field, and the hub was the one exception, installing as the raster wave mark; on a home screen beside Events and Studio that tile read as a different family. The hub now installs as House too (rt-profile's icon route, ICON_VERSION 3), so the rail glyph moves with it. Ruling in the decision register, "The hub installs as a glyph".

ui 0.7.188 (2026-10-03) - the push ask shows only when push can be sent

Cut inside release 2026-10-03 on sdk 0.14.160 (unchanged) and superseded by 0.7.189 before the release merged, so no app ships on it; its change rides in 0.7.189.

ui: usePushSubscription

Changed: a device on a server with no VAPID key reads unconfigured, not off. detectPushState takes the SDK client and reads the key with the state, so the app's own ask is never shown for a push the server cannot send. Before this the ask opened the browser's permission prompt first and learned there was no key second, so a yes landed on unconfigured with a permission granted for nothing. enablePush still asks the browser first within the tap, because Safari refuses a permission request outside the gesture, and reads the key again after it.

Added: serverOffersPush(client), the one read behind that decision, exported for an app that wants to decide earlier.

ui 0.7.187 (2026-09-30) - the sign-in dialog says what it does

The next ui tier after 0.7.186, on sdk 0.14.160 (unchanged), cut mid-week from pull request 202 (its prerelease, ui 0.7.187-pr202.1, is folded here). No app pins it yet; the rt- apps move to it with the next weekly release.

ui: AuthenticationChoice copy

Changed: the sign-in dialog names Google or X. The hosted button reads "Continue with Google or X" and the education point reads "Sign in with Google or X. We set up your wallet for you." Email was never a sign-in method: DEFAULT_LOGIN_METHODS is ["google", "twitter"].

Changed: the third education point no longer promises the keys. A wallet made at sign-in is a Privy server wallet the protocol signs for, and there is no key export, so "your keys, your call" was not true. It now reads "Yours, on record - What you collect is recorded to your account."

sdk 0.14.160 + ui 0.7.186 (2026-09-30) - a new sticker starts at the template's spot

The next tier after sdk 0.14.159 / ui 0.7.185, cut mid-week from pull request 200 (its prerelease, sdk 0.14.160-pr200.1 / ui 0.7.186-pr200.1, is folded here). grammata pins it, since its sticker step is the one placement editor. The rt- apps place no stickers and move to it with the next weekly release.

sdk: defaultPlacement reads the zone's anchor

Added: StickerZone.anchor?: { x, y }, the template's suggested spot for a sticker in canvas coordinates. The API sends it on every Style.stickerZone whose template suggests one.

Changed: defaultPlacement(zone) starts a new sticker at zone.anchor when the zone carries one inside its bounds, and at the centre of the zone otherwise, as before.

ui: no change of its own

Repinned to sdk 0.14.160-pr200.1 in lockstep; StickerEditor picks up the new default through defaultPlacement.

sdk 0.14.159 + ui 0.7.185 (2026-09-28) - bitcoin reads in sats

The next tier after sdk 0.14.158 / ui 0.7.184, cut from the merged source of pull request 192, whose prerelease (sdk 0.14.159-pr192.1 / ui 0.7.185-pr192.1) is folded here. Every rt- app moves to it.

sdk: sats at every size

Changed: formatSbtcDisplay always returns whole sats, however large. An amount of 0.001 sBTC or more used to render as a six-decimal figure (0.004800 sBTC); it now reads 480,000 sats, the way smaller amounts already did. Exactly one sat reads 1 sat. Sats are the smallest unit of bitcoin and the one unit every chain shares, while sBTC is how Stacks wraps bitcoin, so a person reads the bitcoin rather than the wrapper. formatCurrency is unchanged: it still renders the on-chain decimal (0.0048 sBTC) for operator tables.

ui: sats in every component that shows bitcoin

Changed: PriceDisplay and PriceDisplayPair show an sBTC price in sats at every size, through formatSbtcDisplay. With showCurrency={false} they strip sats, or sat for exactly one.

Changed: ConnectWalletButton shows the connected bitcoin (L1) balance in sats rather than as a BTC decimal.

sdk 0.14.158 + ui 0.7.184 (2026-09-26) - a tournament from its contract, and Web Push

The next tier after sdk 0.14.155 / ui 0.7.181. Two branches were cut in parallel from that tier and each numbered its own sdk 0.14.156 and ui 0.7.182; neither reached a deployed app, so they are folded here and this is the first tier that carries both. Every rt- app moves to it.

sdk: one authority for a tournament

Added: the bracket-action command (BracketActionIntent, BracketAction, BracketPool) and rtClient.privy.signBracketAction, with POST /api/privy/sign/bracket-action behind it. Lock, unlock, post and clear results and play-ins, score up to 50 entries, claim and withdraw the pool - all in deny mode. Scoring and claiming are open to anyone signed in; the rest need the deploying address and CREATE_BRACKETS. See rtClient.brackets.

Removed: rtClient.brackets.lock, settle, setChampion and setPlayInResult, the /api/admin/brackets/{id}/lock|settle|champion|play-in-result routes behind them, and rtClient.privy.signBracketAdmin with /api/privy/sign/bracket-admin. A tournament's state comes from its contract: SETTLED from a claim or a withdrawal, the champion from the final's result. POST /api/brackets no longer accepts a 4-team bracket, which no contract could be generated for.

sdk: a round, counted

Added: rtClient.brackets.getRoundTally(bracketId, round) and the BracketRoundTally, BracketTallyRow, BracketTallySide and BracketTallyWindow types, with GET /api/brackets/{id}/rounds/{round}/tally behind them - app key only. For a tournament settled by a count (a song battle): the round's matchups, each song's plays in the window, who went through and whether plays or seed decided it. status is not-open, provisional, counting or final; a final tally is stored once and lists every play that counted. See rtClient.brackets.

Removed: prizeWithdrawnAt from BracketEntryDetails. Nothing wrote it; a withdrawal of the pool is on the ledger.

sdk: Web Push subscriptions, and a push tap can be counted

Added: push on the client. getVapidPublicKey(), subscribe(input), unsubscribe({ endpoint } | { id }) and listSubscriptions(), over the new /api/push/* routes. A subscription belongs to one device in one app, because every app is its own origin. An app-scoped API key may only subscribe devices for its own app. What gets pushed is the notification feed, the same items users.getNotifications returns: each item goes through the app it belongs to, or through the hub when the person has no subscription in that app, and never through both.

Added: the PushPayload type and PushPayloadSchema. What a service worker receives in its push event: the NotificationItem itself, so the app writes the notification in its own words, a more count of items folded into the push, an app-neutral fallback sentence to show for any kind the app does not recognise, and a badge count.

Added: two notification kinds. match.resolved: a game match you sat in was decided without you winning it, with NotificationItem.match (result: lost or drew, and the winner on a loss). A win is the winner's own act and is never reported to them; bracket scores are not notifications. asset.reviewed: your submitted sticker or style set was approved or sent back, with NotificationItem.review (assetKind, name, approved). The reviewer's note is not carried; it waits in Studio. ASSET_REVIEWED joins ProtocolEventType. A client that renders every kind must handle both, or show a neutral line for kinds it does not know.

Nothing is sent yet. The server ships with push switched off. Subscribing works, and the key route answers 503 until a VAPID key is configured, which a client reads as "push is not offered".

Added: PushPayload.deliveryId and push.recordOpen(deliveryId), over the new POST /api/push/opens. A service worker carries the id on the URL a tap opens, as ?via=push&pd=<deliveryId>; the open is recorded once, only against the caller's own delivery, and answers { recorded } either way. Nothing else changed for a client that ignores it.

ui: usePushSubscription, and RisingTideProviders records push taps

Added: usePushSubscription({ client, appId, signedIn }) in @rising-tide/ui/react. It returns the device state (unsupported, needs-install, unconfigured, denied, off, on), shouldAsk for the app's own soft ask, enable(), disable() and dismissAsk(). enable() is the only thing that opens the browser permission prompt, so call it only from a tap on the app's own yes: a person who blocks the prompt cannot be asked again. On iOS in a browser tab the state is needs-install, because only an app on the Home Screen can receive push there. Call disable() from sign-out, before the session ends.

Added: the provider stack reads ?via=push&pd=<id> on load, waits for the sign-in answer, records the tap with push.recordOpen when someone is signed in, and removes both parameters from the URL either way. Other parameters, such as an app's own ?inbox=1, are left in place. An app only has to put the marker on the URL its service worker opens. Pins sdk 0.14.158.

conformance 0.11.2 (2026-10-04) - the tree carries only the tarballs it installs

Added: rule 35, tarballs-are-pinned. Every rising-tide-*.tgz the repository tracks is one package.json pins with a file: dependency, and every one it pins is tracked. The first half is the leftover a repin forgets to delete - nothing reads it, so release 2026-10-03's sweep found 201 of them (125 MB) across five apps; the second half is the pin that fails npm ci on every machine but the one that packed the file. Ships at error, measured clean against every app that carries the tarballs. Context gains packageJson and tarballs (the tracked list, from git, with a root listing where there is no checkout) for anyone writing rules against it, and syntheticContext reads an x-tracked-tarballs field off a JSON control so the rule can be controlled like every other. Every app repins to it inside release 2026-10-03.

conformance 0.11.1 (2026-09-25) - an unrelated comparison is not a gate

Fixed: no-hand-rolled-affordability flagged comparisons that had nothing to do with money. A file that reads a balance to display it and also compares an unrelated count (Bottle Drop's profile menu badges its unread count with unreadCount > 9) read as a hand-written gate. A comparison now counts only when its line mentions a value that came from useWalletBalance, directly or through one assignment.

conformance 0.11.0 (2026-09-25) - the push surface

Added: offers.push, and three rules under it. An app that subscribes devices with usePushSubscription declares it, and conformance now reads the app's service worker as well as src/ (serviceWorker in the config, default public/sw.js).

  • push-worker-shows-every-push: the worker has a push listener that calls showNotification inside event.waitUntil, and reads the payload's fallback for kinds it does not know. Safari revokes push permission from a site whose push shows nothing.
  • push-worker-handles-taps: a notificationclick listener that focuses or opens the app.
  • push-unsubscribes-on-sign-out: an app that subscribes also calls disable(), so a device stops receiving the previous person's notifications.

No app in another repo declares push yet, so nothing changes for one that upgrades.

sdk 0.14.155 (2026-09-25) - a record for one game

Added: game on rtClient.games.getRecord(address, { game }), and ?game= on GET /api/games/records/{address}. Without it the record spans every game, as before; with it the record is that game alone - played, won, drawn, lost, hosted and prizes - counted from the same finished matches the leaderboard ranks. See Games.

ui 0.7.181 (2026-09-25) - lockstep with sdk 0.14.155

No change of its own; repinned to sdk 0.14.155.

sdk 0.14.154 (2026-09-25) - a leaderboard per game

Added: rtClient.games.getLeaderboard(game, { limit }) and the GameLeaderboard and GameLeaderboardEntry types. Added: GET /api/games/leaderboard, the route it calls - public, app key only. It ranks every address that finished a match of one game by wins, then draws, then fewest losses; tied lines share a rank. Prizes are the payouts claimed in that game. See Games.

ui 0.7.180 (2026-09-25) - lockstep with sdk 0.14.154

No change of its own; repinned to sdk 0.14.154.

sdk 0.14.153 (2026-09-24) - a claimed invite reads as claimed

Added: claimed on WaitlistStatus. Present and true once the invite under an INVITED request has been claimed. Before this a claimed invite looked exactly like a revoked or expired one

  • no claimUrl - and a status page told someone already in that their invite had lapsed.

ui 0.7.179 (2026-09-24) - lockstep with sdk 0.14.153

No change of its own; repinned to sdk 0.14.153.

sdk 0.14.152 (2026-09-24) - open spots on the waitlist

Added: rtClient.waitlist.openSpots() and the WaitlistOpenSpots type. It reports how many signups the deployment admits on the spot right now; open is null when it admits none. Added: GET /api/waitlist/open-spots, the route it calls - public, app key only.

While spots are open, signup returns a request that is already INVITED, so status carries a claimUrl at once. See Waitlist.

ui 0.7.178 (2026-09-24) - lockstep with sdk 0.14.152

No change of its own; repinned to sdk 0.14.152.

sdk 0.14.151 (2026-09-23) - a trophy drawn from a sample match

Added: rtClient.gameStyles.generateLivePreview(styleSetId, sample) and the GameTrophySample type. It draws a trophy in a GAME style set from a sample match, through the same content builder a settled match is drawn with, and answers in the page preview's shape, so extractSvg reads it unchanged. The protocol replays the sample under the game's rules: an illegal move, a bomb on an empty square or at a Drop Four table, and inputs that end before a line or a full board are each a 400 naming the problem, and a style set from another domain is DOMAIN_MISMATCH. A sample can therefore only draw a board a real match could reach.

Added: POST /api/styles/games/preview/{id}, the route it calls. A read: the public key alone works, and it writes nothing.

ui 0.7.177 (2026-09-23) - lockstep with sdk 0.14.151

No change of its own; repinned to sdk 0.14.151.

sdk 0.14.150 (2026-09-21) - a seat can leave a lobby

Added: leave on the game-action command. A seated player leaves the match being formed and the contract sends back exactly the seat's recorded pool share, in the seat's currency; the creator legs were paid at buy-in and stay paid. On the wallet path the intent carries currency and poolShare and the builder asserts an exact deny-mode condition on the CONTRACT for that amount (leave-seat moves nothing of the signer's). The Privy route reads the signer's seat from the projection instead and answers 409 SEAT_NOT_FOUND rather than sign a call the contract would refuse.

Added: GAME_REFUND to TransactionType. The refund is its own transaction type so a revenue report never counts it as a payout won.

Lobbies deployed from this tier carry a token URI a wallet can read. get-token-uri returns the API's SIP-016 document for the contract (/api/contracts/{deployer}/{name}/tokens/{n}) and the app page rides inside it as external_url; lobbies deployed before it point at the app page and are repointed with set-detached-uri. The fee also freezes from the first seat, not the first started match.

ui 0.7.176 (2026-09-21) - lockstep with sdk 0.14.150

No change of its own; repinned to sdk 0.14.150.

conformance 0.10.1 (2026-09-21) - the affordability matcher, corrected against the estate

no-hand-rolled-affordability keyed on a balance read plus a disabled prop, and that was wrong in both directions. Run against every app that carries this package, it missed a gate that hides its control behind an early return, and flagged a hub reading a balance to DISPLAY it beside a button disabled on request status - a file with nothing to fix.

It keys on the COMPARISON now, which is the thing that actually decides. Three exclusions, each measured rather than guessed:

  • ===, because a status check is not a gate.
  • an unspaced < or >, because TypeScript generics are written useState<string>; the unspaced form read every type parameter as a comparison and flagged seven display panels.
  • a comparison against 0, because sbtc > 0 asks whether a thing EXISTS. Sixteen of the nineteen comparisons in the estate's balance-reading files are that shape.

SHARED_GATE also matches use*Affordability. Every adopting app wraps the primitive in a hook named for the action it guards, so the component consuming the verdict never names usePaidAction - and matching only the protocol symbol read all of them as ungated.

conformance 0.10.0 (2026-09-21) - a paid control must know the user can pay

Added: paid-write-consults-the-gate. A file that dispatches a protocol write - useDeployProduct, useCommand, commands.submit, or a rtClient.privy.* sign call - must also consult usePaidAction. Twenty controls in the estate shipped with no balance check of any kind, and the failure is not a refused button but an enabled one: the user signs, the chain refuses for insufficient funds, and the message names neither the gate nor the cause.

Added: no-hand-rolled-affordability. A file that reads a wallet balance AND disables a control must go through the primitive. Every hand-written copy in the estate got at least one of three things wrong - gas is always STX even when the price is in sats; an errored read returns 0 and reads as an empty wallet; a disabled query resolves instantly with 0 and disables every paid button for a signed-out visitor.

Added: the paidActions surface. Declared in conformance.config.json, or inferred from the write surface when undeclared. declares-paid-actions-surface checks the declaration both ways.

Neither rule names a component. Both key on protocol-owned symbols, because a written list stops covering what ships next - which is how three of the four instances of this defect accumulated.

sdk 0.14.149 / ui 0.7.175 (2026-09-21) - one primitive for every paid button

Added: usePaidAction in @rising-tide/ui. One question, both legs: the face amount in its own currency, and gas in STX. A sats-priced action needs an STX balance too, and six of the eight hand-written checks in the estate missed exactly that - a wallet funded entirely in sats passed every gate and still could not transact.

const pay = usePaidAction({ address, amount: priceSats, currency: 'sBTC' })
<button disabled={!pay.canAfford}>

canAfford is false ONLY when a balance is known and short, so an in-flight or errored read never disables a button. shortfall names what is missing and in which currency, so the copy can say so; held gives both balances for stating both sides. legsFor and shortfallOf are exported as pure functions.

An action with no price still costs gas: pass amount: 0 and it is checked anyway. A deploy passes gasUstx: deployGasUstx(contractBytes).

sdk 0.14.148 / ui 0.7.174 (2026-09-20) - a deploy is priced by its own size

Fixed: deploy estimates were a median of other people's fees. The market signal took the median FEE of the last 50 public testnet deploys, of unknown size, and used it as a price for your deploy of known size. Two faults at once: a deploy's fee scales with bytes, so a median fee is not a price; and a public median is spammable, with one sender currently holding 22 of those 50 and paying a flat 500,000 uSTX on 2.3 KB contracts. A 16.5 KB deploy quoted 404,076 uSTX against a chain that takes a tenth of it. The signal now measures a per-byte RATE from the sample (one vote per sender, a low percentile, bounded), and prices your contract by its own length. Measured on the same sample: 404,076 becomes 167,220.

Added: deployGasUstx(contractBytes) in the SDK. The most a deploy of that size can be charged, from DEPLOY_RATE_CEILING_USTX_PER_BYTE - an upper bound rather than an estimate, so a gate built on it cannot quietly become wrong when the rate moves. PROTOCOL_DEPLOY_FEE_FLOOR_USTX mirrors the other end.

Changed: a deploy's ceiling scales with the contract. TX_BOUNDS.deploy.ceiling was flat and size-blind, so it never bound in practice. A deploy is now clamped to contractBytes x DEPLOY_RATE_CEILING_USTX_PER_BYTE, which is the same arithmetic deployGasUstx gives a client. The ceiling outranks the feePriority multiplier: a multiplier is a request and a ceiling is a protection.

sdk 0.14.147 / ui 0.7.173 (2026-09-20) - what a contract call costs, in one place

Added: CALL_GAS_USTX and callCostUstx(faceUstx) in the SDK. A face amount is never the whole price - every broadcast burns gas, and gas is always STX even when the thing being bought is priced in sats. A client that checks only the face amount passes a user who holds sats and no STX, who then cannot transact. callCostUstx adds the gas leg; a sats-priced call passes 0 and still gets a number back.

Added: useAffordable in @rising-tide/ui. One primitive for "can this address cover this action", replacing the per-app copies. It takes the ALL-IN in base units and one currency; canAfford is false ONLY when the balance is known and short, so an in-flight or errored read never disables a button. affordability and balanceIsKnown are exported as pure functions for anyone gating outside React.

Changed: TX_BOUNDS is exported from rt-api's fee estimation service. Its contract-call floor is the fee a Privy-signed call actually pays, since the blend beneath it has never cleared the clamp. The SDK mirrors that floor and a guard fails if the two stop agreeing.

sdk 0.14.146 / ui 0.7.172 (2026-09-20) - the match read carries its claim transaction

Fixed: GET /api/games/[id]/matches/[matchIndex] returns awardClaimTxId. The list route has always carried it and the single-match read dropped it, so the same row answered two different shapes depending on which door you came through. GameMatchState gains the field; nothing else changed.

sdk 0.14.145 / ui 0.7.171 (2026-09-20) - nothing signs in allow mode

Fixed: a claim no longer warns the winner that any of their assets may move. claim and claim-draw broadcast in post-condition allow mode, which makes the wallet warn about the signer's whole balance at the one moment nothing of theirs moves at all. Both signers - the wallet path's buildGameActionTransaction and POST /api/privy/sign/game-action - now broadcast in deny mode with a condition on what the CONTRACT sends back.

Changed: a claim states the floor it expects, not an exact amount. GameAction's claim and claim-draw take an optional currency and payout; the payout becomes Pc.principal(contract).willSendGte(payout). A floor, never an exact amount: a kitty grows when someone uses an item, so a read that trails the chain is always low and an exact condition would abort a legitimate claim. The Privy route reads the kitty itself and ignores any client number. contractPayoutPostConditions is exported for anyone building the call by hand.

Changed: four owner-only calls stopped warning for no reason. set-product-info, set-max-supply, set-detached-uri and lock-detached move no assets and were still signing in allow mode. They now carry deny with an empty list, which is the correct form when nothing moves - deny aborts only an uncovered transfer - and raises no warning. A fee-less deploy says the same thing explicitly.

sdk 0.14.144 / ui 0.7.170 (2026-09-20) - the bomb is a spectator item

Changed: only a spectator can throw a bomb. In Tic-Tac-Boom the bomb belongs to the crowd, not the players. use-bomb on a lobby deployed from the new template is refused on-chain for either seated player (err-players-cannot-bomb) and succeeds for any other signed-in caller. Existing lobbies keep the contract they deployed with; a new lobby gets the new rule.

Added: itemAudience on the lobby read. GET /api/games/[id] returns gameLobbyDetails.itemAudience - 'spectator' or 'player' - so a client gates the item affordance on a declared value rather than hard-coding the bomb. Every item declares an audience; the bomb declares spectator, and nothing ships as player yet.

sdk 0.14.143 / ui 0.7.169 (2026-09-20) - a host can sit at their own table

Fixed: a buy-in's post condition states what the signer actually sends. A lobby keeps the pot share and pays one leg per creator, and it never transfers a principal to itself - so a host seated at their own table sends only the pot share. Both signers (the wallet path's buildGameActionTransaction and POST /api/privy/sign/game-action) demanded the face amount, and the chain aborted the host's seat on its post condition. A join and a bomb now carry the amount less any creator leg addressed to the signer; signerSpend is exported for anyone building the call by hand.

Added: feeLegs on the lobby read. GET /api/games/[id] returns gameLobbyDetails.feeLegs

  • one { address, percentOf100 } per creator leg, the integer percent frozen at deploy - beside the seats. Pass them as feeLegs on a join or use-bomb game action; without them the face amount is assumed, which is right for anyone who is not a creator of the lobby.

sdk 0.14.141 / ui 0.7.167 (2026-09-20) - the ladder is climbed in order, and it reaches the games

Changed: the ladder is seven rungs, earned in order. Every ladder rung names the rung before it, and the projector awards a rung only once that one is held - a counter can run past its threshold early and the rung waits, then lands the moment its precursor does, in the same transaction and on the same event. The two game apps join the climb: page-1 ("First page") now opens Tic-Tac-Boom; match-1 ("First match", a match played there, any result) opens Drop Four; pot-1 ("Graduated", a match won in either game) pays the invite that page-1 used to. A won match is counted from INSTANCE_SCORED for the winner only, per app, and the rung reads both apps' counts; a draw counts for nobody.

Changed: the return invite renews every ten days. return-5 still pays on the fifth distinct active day; the invites after it come every ten further days rather than every five. LadderRung.renewsEvery reads 10.

Added: precursor on LadderRung (GET /api/achievements/ladder) - the slug of the rung that must be held first, null on the first rung and on every return rung. Join it with a shelf to mark a rung locked and say what it waits on. The Hub's Achievements page does.

Added: the activity feed knows a match. INSTANCE_SCORED and INSTANCE_PRIZE_CLAIMED payloads carry domain (PREDICTION or GAME); a GAME event derives match.won, match.lost, match.drawn or pot.claimed, and a GAME PRODUCT_DRAFTED derives lobby.opened. A row without a domain is a bracket, as every earlier one was. AppActivitySummary and UserActivityTotals gain lobbiesOpened, matchesWon, matchesLost, matchesDrawn and potsClaimed. ui unchanged in behavior; bumped for lockstep.

sdk 0.14.140 / ui 0.7.166 (2026-09-19) - playing a lobby, and a player's record

Added: the game-action command. rtClient.commands.submit({ kind: 'game-action', ... }) signs one lobby action on either session - join (a buy-in, with currency and amount), start, move (input, the square), claim and claim-draw (matchIndex), and use-bomb (square, currency, amount). A Privy session routes to the new sign route POST /api/privy/sign/game-action; a connected wallet builds the same call client-side through buildGameActionTransaction. A join and a bomb carry an exact deny-mode post-condition for the spend; a claim broadcasts in allow mode because the kitty leaves the contract, not the signer. Types: GameActionIntent, GameAction, GameActionParams, GameActionTransactionParams.

Added: rtClient.games.getRecord(address) over GET /api/games/records/{address} - one address's GameRecord: matches played, won, drawn, lost and hosted, and the prizes claimed in each currency, projected from INSTANCE_SCORED and INSTANCE_PRIZE_CLAIMED. An address that never played answers with zeros. GameRecordSchema under @rising-tide/sdk/schemas. ui unchanged in behavior; bumped for lockstep.

sdk 0.14.139 / ui 0.7.165 (2026-09-19) - the GAME domain and game lobbies

Added: the fourth product domain. ProductDomain gains GAME everywhere it is spelled (ProductDomainSchema, the list-param and detail-row unions, domainScope). A game lobby is a SmartProduct of that domain hosting a series of matches; a match is a ProductInstance created when it starts and owned by the lobby contract until the winner claims the trophy.

Added: rtClient.games (GamesService): list, getById (the lobby with its gameLobbyDetails and the seats of the match being formed), getMatches, getMatch (the board, the moves, whose turn, the kitty), createDraft (accepts the smart-lobby recipe - items: true composes the bomb), updateDraft, deleteDraft, getContributions. Routes: /api/games, /api/games/{id}, /api/games/{id}/matches, /api/games/{id}/matches/{matchIndex}.

Added types and schemas: SmartProductGame (+ isGame), GameListParams, GameLobbyDetails, GameLobbySeat, GameMatchDetails, GameMatch, GameMatchState, GameMove, GameParticipant, GameMatchStatus, GameResult, each with a matching schema under @rising-tide/sdk/schemas. ProductInstance gains gameMatchDetails. ui unchanged in behavior; bumped for lockstep.

ui 0.7.164 (2026-09-16) - one shell

Removed: ShellLayout and TopBar from @rising-tide/ui/shell. AppShell is the package's one shell. The last consumer, rt-admin, moved onto it (no rail, its own subnav in nav, four bottom tabs on a phone and a full-nav drawer from the header), and a second shell that could not carry a media query had nothing left to serve. SubNav, SubNavItem, FootBar, the rail and the hooks are unchanged; an app importing ShellLayout or TopBar fails to compile, which is the intended signal.

Added: exact on ActivePathTarget and SubNavItem. A section landing page with siblings under its own path (/system beside /system/transactions) lit on every child under prefix matching, so two items read as current at once. exact: true lights the item on its own href alone; match prefixes still apply, and a tab derived from the same section keeps the prefix match it wants. SDK unchanged at 0.14.138.

sdk 0.14.138 / ui 0.7.163 (2026-09-15) - a renewal invite can only be spent on the pool

Invite gains addressing (InviteAddressing: ANY | WAITLIST_ONLY), set when the invite is minted and never patched. It is orthogonal to claimBinding (who may claim) and holderUserId (who may send): it says where the send may go. An operator mint is ANY. A protocol mint takes it from the source: the Graduated invite (page-1) is ANY - bring whoever you want is the prize - while a return-5 renewal and the weekly drip are WAITLIST_ONLY. One-time rewards buy loyalty; recurring supply buys growth.

Enforced server-side. rtClient.invites.send on a WAITLIST_ONLY invite is refused with INVALID_ADDRESSING (400) whether or not a recipient is typed, and whoever sends it. The one door is rtClient.invites.sendToWaitlist. Render a held WAITLIST_ONLY draft with the pool as its only action and no bind form - a form that can only fail is worse than none. Existing rows are ANY.

sdk 0.14.137 / ui 0.7.162 (2026-09-15) - the waitlist pool, and a pick that never sees a contact

New: members can bring in someone who is waiting. A waitlist applicant may now agree, on the form, to have their two answers read by signed-in members (WaitlistSignupInput.publicConsent, opt-in, absent is never consent) and may choose a displayName to be shown in place of their contact. An operator publishes a consenting row to the pool (rtClient.waitlist.publish, withdraw); any member reads it (rtClient.waitlist.pool); and a member holding a DRAFT invite spends it on an applicant with rtClient.invites.sendToWaitlist(inviteId, signupId).

What the pool serializes is the contract: PooledApplicant is id, displayName, currentWork, pitch, interests, experience - no contact, no channel, no dates, no total. The pick goes up as a signupId; the server resolves the row, binds the invite to the contact it holds (90-day expiry, INVITE_SENT payload carries waitlistSignupId) and the picker never learns how to reach the person. Two picks of one applicant in the same second: one wins, the other gets TAKEN (409) with its invite untouched - draw that as good news, not an error.

Also: WaitlistSignup gains notifiedAt, publicConsent, visibility, displayName and pooledAt; WaitlistApproveResult and WaitlistPickResult carry notifiedAt, which is when the applicant was emailed that a spot opened (an EMAIL row, where the deployment has a mail provider) or null. Build against null: the claim link is still the delivery when nothing was sent.

conformance 0.9.7 (2026-09-14) - share cards and chain-bound text get their rules

Five rules and two declarations, each controlled from a defect that shipped.

Share cards - the picture and words a pasted link shows - are read by nothing internal, so their defects were found by a person fetching og tags by hand. Four rules now fail the commit that would reintroduce the file-level half of that class, in the app that owns it: no-metadata-convention-files (an opengraph-image.tsx silently outranks openGraph.images config and resolves against the deployment host), card-url-ends-png (X refuses an extensionless image URL whatever the content type), web-host-normalized (a raw NEXT_PUBLIC_WEB_HOST read emits a doubled slash on the deploy whose value ends in one), and entity-routes-own-metadata (a card.png route whose segment has no metadata owner shows that person's card beside the site blurb). They apply when an app declares offers.shareCards, or when a card route is found; declares-share-cards-surface checks the declaration both ways. The API's health tick reads the deployed world for the same class.

chain-text-is-folded: text a person types that reaches a Clarity (string-ascii) literal is folded in the box (foldToAscii from @rising-tide/sdk), so an accent comes off while they watch rather than in a refusal. The derived half needs no declaration - a text input bound to a person's or a contributor's display name must fold - and found two collaborator-name inputs on its first run that no list had named. The declared half reads offers.chainText, a list of the app's compose surfaces: each must fold, a file that folds and is not listed is a surface nobody re-checks, and a listed file that is missing is a stale declaration. declares-chain-text-surface checks the list.

Controls may now describe more than one file: a line reading // @file src/app/x.tsx starts a new file at that path, which is how a rule about where a file sits is controlled from the shape of its defect.

All ten apps green on the first scored run, with three edits: two card routes read the host through the normalizing module, and two collaborator dialogs fold the name.

ui 0.7.161 (2026-09-12) - a refused display name reaches the person who typed it

Fixed: a signup refused for its display name closed the welcome dialog and said nothing. The server validates a display name as Clarity text, because it is frozen into the creator-info map of every contract the account deploys, and answers INVALID_INPUT with a sentence naming the field. ProtocolUserProvider stored that sentence in error, closed the dialog and dropped the pending address, and no dialog in the estate rendered it - so a person whose name carries an accent was signed in with no account and no reason.

The provider now reopens the welcome dialog with the refusal in error when the TYPED name is refused (reopensForRefusal); a skip's fallback name is always legal, so a refusal of it never reopens and bottle-drop's auto-skip cannot loop. Every app's welcome dialog lands on the profile step with the sentence under the name field, and rt-profile's settings page and edit modal show the server's sentence in place of "Failed to update profile".

The larger half of the fix is in the apps, on the rule the sdk 0.14.136 entry states: fold the look-alikes where the person can watch, refuse the rest at the boundary. Every welcome dialog, rt-profile's two name inputs and grammata's title, subtitle and body now run foldToAscii on the way into state, so an accent comes off in the text box rather than in a 400.

useEntitlement's example names COLLECT_EDITIONS, an entitlement that exists. SDK unchanged at 0.14.136.

ui 0.7.160 (2026-09-11) - every app renders its page on the server again

Fixed: every app on RisingTideProviders served an empty page body. The access curtain (AccessGate) returned nothing while the viewer's verdict was in flight, and on a server render the auth session has never restored, so the verdict is always in flight: the HTML of six apps carried no h1, header or section, first paint waited for the client to hydrate and ask (measured at 3.1s against a 56ms time to first byte on the landing), and nothing was crawlable by content.

The curtain now renders the room while the question is open and replaces it with the teaser only on a resolved LOCKED verdict. The one viewer this costs is the ungranted one at a gated app, who sees the room for the beat it takes their verdict to arrive; every action is still refused server-side. An app that prefers a blank to that beat passes loading to AccessGate, which renders instead of the room. tests/unit/providers/server-render.test.tsx renders the whole provider tree with renderToString, layer by layer, and names the layer that eats the child if one ever does again.

SDK unchanged at 0.14.136.

sdk 0.14.136 / ui 0.7.159 (2026-09-10) - non-ASCII no longer reaches the chain silently

Fixed: text bound for a Clarity (string-ascii N) parameter was corrupted without any error. Cl.stringAscii does not reject non-ASCII - it truncates every UTF-16 code unit to its low byte. There is no throw, no warning, and the ClarityValue still reports the original string when you read .value back, so the damage appears only in the serialized bytes, by which point it is minted into an edition that cannot be edited:

"HONK " + U+1FABF    ->  484f4e4b203ebf        "HONK >" + U+00BF
"It" + U+2019 + "s"  ->  49741973              "It" + U+0019 + "s"
"caf" + U+00E9       ->  636166e9              a byte that is not ASCII at all

The emoji is the memorable case; the curly apostrophe is the likely one, since every phone keyboard produces it by default, and it lands as a control character rather than a wrong letter.

New: @rising-tide/sdk exports the charset rule - foldToAscii, isClarityAscii, findNonAscii, nonAsciiMessage and assertClarityAscii. The two halves are deliberately asymmetric:

  • Compose surfaces FOLD. foldToAscii rewrites the characters that have an exact ASCII equivalent - smart quotes, dashes, the ellipsis, non-breaking spaces, accented letters, the ligatures NFD leaves whole. Call it on the way into component state so the writer watches the substitution happen in their own text box. Anything left over (an emoji, a CJK character) has no ASCII it could honestly become, so it stays visible and holds the submit closed.
  • Write boundaries REFUSE. Every transaction builder that feeds a (string-ascii) parameter now asserts before it encodes - buildReservationTransaction, buildSetProductInfoTransaction, buildSetDetachedUriTransaction, buildPublishOpenLetterTransaction and buildPublishFeedbackTransaction all throw naming the offending characters. The matching Privy signing routes return a 400 rather than signing. Folding server-side would be the same rewrite with nobody watching, so the server never folds.

Also fixed, as a consequence: the character counters in a composer were counting UTF-16 units against a field that stores bytes, so a surrogate pair spent two of the writer's allowance and bought two bytes of garbage. The two counts agree once the text is ASCII, which is now the condition for submitting at all.

If you build on the SDK: a value your app previously sent with a curly apostrophe now throws. Fold at the input (foldToAscii), surface what is left (findNonAscii), and gate your submit on it being empty. Do not widen the Clarity type - string-utf8 costs more per character and changes the frozen contract shape.

ui 0.7.158 (2026-09-10) - the curtain waits for the token

Fixed: the entry curtain locked out users who hold a grant. GET /api/apps/{id}/access resolves the caller from the bearer token, and authTokenReady lags useProtocolUser().isLoading - so the curtain asked as soon as auth resolved, the route answered UNAUTHENTICATED, and a gated room came back LOCKED. staleTime: 60_000 then kept the viewer at the door. Found by walking deployed testnet: an account holding the Bottle Drop grant was shown the locked screen on Bottle Drop.

useAppAccess and AccessGate take authTokenReady now. An authenticated viewer HOLDS until the token is attached; an anonymous one is asked immediately, since there is no token coming. Callers outside the provider - rt-events' useCanPublishEvents is the one in the estate - must pass it or the verdict never resolves.

The lesson is the shape, not the line: a gate that reads identity from a token has a window where the token is missing, and in that window it does not fail open, it fails shut.

ui 0.7.157 (2026-09-10) - the entry curtain is universal, and it stops asking the catalog

Fixed: a gated app the viewer has no grant for now resolves LOCKED instead of UNKNOWN. useAppAccess asked the per-user catalog read, which OMITS a gated room the operator chose not to advertise (catalogTease: false). The lookup missed, the miss read as ambiguity, and the gate fails open on ambiguity by design - so hiding a room was how you unlocked it. Both the anonymous and the authenticated path now ask GET /api/apps/{id}/access, which resolves the caller when a token is attached. catalogTease goes back to meaning what it says, and the gate no longer reads it at all.

Changed: RisingTideProviders renders the entry curtain for every app. Mounting was per-app homework and three apps of ten never did it, which is what opt-in produces - ModeGate sits in the same provider and has never been forgotten, because it cannot be. The curtain mounts below ProtocolUserProvider, since it needs a resolved viewer, and inside ComingSoonGate, since a pre-launch wall applies to everyone. What DRAWS is server state: a PUBLIC app resolves OPEN and the curtain never appears, so the hub is exempt because it has no door rather than because a list names it.

New: accessGate on the provider config. teaser supplies an app's own locked screen (AccessTeaserProps = appName, authenticated, onSignIn); selfGated opts an app out where it already gates its own entry more strictly (rt-admin wants an entitlement, not an app grant). An app whose reads are merely open is NOT self-gated. Forgetting the block draws a curtain, which is the recoverable direction.

Changed: the shared teaser addresses both readers and carries a sign-in control. It told a visitor to sign in with no control to do it, and now branches on authenticated and opens the shared auth dialog. The curtain replaces the app's chrome, so the locked screen has to be self-sufficient.

Server enforcement is unchanged. requireAppAccess / requireAppAction already 403 an ungranted write on all twelve acquisition routes. This is disclosure - telling somebody the room is locked before they spend effort in it.

ui 0.7.156 (2026-09-10 - SUPERSEDED by 0.7.157)

The same change as 0.7.157, one build earlier. Packed and installed into all ten apps in the same session, then rebuilt as 0.7.157 before anything was committed, so no tarball or lockfile in git ever carried it and no consumer ships on it. Recorded so the sequence reads as a skip rather than a gap.

sdk 0.14.135 + ui 0.7.155 (2026-09-09) - the touch floor follows the finger, and the shell gets a tablet

Changed: the 44px touch floor is keyed on POINTER TYPE, not on screen width. Every size in the shared Button releases its floor at [@media(pointer:fine)]: instead of sm:. A width breakpoint is not a smaller version of the right rule, it is the wrong axis: an iPad reports pointer: coarse at 820 CSS px and at 1180 alike, so releasing at 640 handed the mouse-sized control to every tablet, every tablet in landscape, and every phone turned sideways. A walk of thirteen deployed surfaces at four viewports found the floor holding on exactly one app - the one somebody had walked by hand.

New: --rt-touch-floor, in @rising-tide/ui/theme. 44px on a coarse pointer and auto otherwise. It exists because the shared components authored in inline style objects cannot carry a media query of their own, and now opt in by reading it: minHeight: &quot;var(--rt-touch-floor, auto)&quot;. SubNavItem is the first consumer - a subnav row is a destination rather than chrome, and on a touch device it is the only way to reach several of them.

Changed: checkButtonContract FAILS a floor released by screen width. A fork that releases at sm: now fails the contract rather than passing it, and the failure names the axis instead of saying no.

New: AppShell has a tablet band at 768-1023. The rail and subnav appear there, and the tab bar STAYS - the band is a touch device, and the subnav arriving beside the tabs is an addition rather than a replacement. Below 1024 the shell had exactly one layout, so iPad portrait got the phone shell: rt-profile lost four app links and eleven subnav destinations, and /notifications had no reachable link at all, its only anchor being the hidden subnav.

Changed: the cross-app rail names its controls. Every rail tile carries aria-label beside title. A title is a tooltip a touch device never shows and assistive tech treats as a last resort, so six icon-only launchers had no accessible name in practice. The app-store toggle also reports aria-expanded.

Changed: the rail says what each app calls itself. bottledrop and grammata set their own wordmark in lower case on every surface they own; the rail said Bottle Drop and Grammata beside them. App.name is unchanged - the map in the rail is where a product's own voice wins. Rising Tide Studio joins Hub and Events in showing an unprefixed Studio.

Changed: storage status lines name no vendor. Saved to Arweave is Saved to permanent storage, and the same for the saving, pending and needs-wallet variants. Destinations are plural by design, so a vendor name inside a status sentence is a string with an expiry date. The name lives in STORAGE_COPY.destinations, which is the entry that is about a particular destination.

Fixed: the Save As row wraps. In its archived state it renders a status chip plus two verbs of the same family, inside a card column about 180px wide on a phone - which squeezed Save again to 45px and Save As... to 41px, each breaking to three lines inside a 44px box.

Fixed: the testnet banner keeps its offer on a phone. The short form is Test network, no real money. Tap for free test funds. The first-sentence split dropped the half that carries the fact a reader cannot get any other way - that free test funds exist and the strip is how to reach them - on the one device where Tap is the literal gesture.

conformance 0.9.6 (2026-09-09) - a floor released by width is a defect

New rule: touch-floor-keyed-on-pointer. It does not say a control must be 44px. It says something narrower: an app that DECLARES a touch floor must not release it at a screen-width breakpoint. The rule is inert in an app that never writes a floor, and it fires on the exact shape that reads as correct - the floor is there, it is 44px, and it is released so desktop keeps its density - while getting the question wrong. Measured across the estate on its first run: 37 hits in four apps.

Changed: ds-button-extension-contract expects the pointer release. The button contract and the new rule now agree; before this one asked for the shape the other flags.

sdk 0.14.133 + ui 0.7.153 (2026-09-06) - the protocol has no storage wallet

Removed: arweave.getWalletBalance() and GET /api/arweave/wallet-balance. There is no protocol storage balance to report. Storage is the owner's: it is held on the owner's storage wallet, written under the owner's key, and users.getStoragePosture() is where a caller asks what one address has. The endpoint reported a platform wallet that no longer writes anything, and the key behind it is leaving every environment.

Removed: the Arweave storage provider. The server holds no key for a permanent destination and now cannot acquire one - @ardrive/turbo-sdk is gone from the API's dependencies, and asking the storage service to land a file at ARWEAVE is refused by name rather than routed. Reads of items written under the old model are unaffected; those rows are permanent and still say ARWEAVE.

Changed: pages are counted against a measured figure. What one Save As writes to a destination was pinned at an estimate of 89,700 bytes and is measured at 45,926 - the mean over 28 editions in the permanent store, counting the manifest and excluding the social card, which a Save As does not carry. The estimate was 1.95x too high, so every "about N more pages" a user sees roughly doubles. It converts a price now, not only an allowance, which is why it had to stop being a guess.

Changed for operators: AdminStoragePosture.lookup answers whether an address can write. It carries a write block - the bytes one Save As needs, what they cost, and canWriteNow with the pocket that would pay. null means the bundler did not answer, and unknown is never rendered as a yes.

sdk 0.14.132 + ui 0.7.152 (2026-09-06) - buying more room, behind a flag

New: users.createStorageCheckout({ amount }). A card payment that adds room to the caller's own storage wallet at a destination, via POST /api/users/me/storage/checkout. amount is in the currency's smallest unit (cents for usd) and must sit inside the offer the posture carries. A hosted session answers with a url to send the browser to; an embedded one answers with a clientSecret. The credits land on the caller's address, never the protocol's - the protocol does not hold a storage balance and this does not give it one.

Changed: UserStoragePosture gains paidRail and credits.aboutPages. paidRail.offer is the whole offer - currency, the destination's minimum and maximum, and suggested amounts each carrying what they buy in pages. It is null whenever the rail is off, which is the contract that matters: with the rail off the server computes no price at all, so there is nothing for a client to render or leak. credits.aboutPages says a bought balance in the same pages the free allowance is said in. Both are additive; a client that ignores them is unaffected.

Flagged off by default. The rail is STORAGE_PAID_RAIL_ENABLED on the API, and testnet does not set it. With it off, the checkout route answers 409 with a reason that names free storage and carries no amount, and a save that runs out of room says so and stops.

New in @rising-tide/ui/storage: useStorageTopUp. Starts a checkout and, on the way back, polls the posture until the balance changes or two minutes pass. SaveAsButton takes an optional onNeedsRoom - passing it is what turns "out of room" into an offer, so a host with no paid rail simply leaves it out.

sdk 0.14.131 + ui 0.7.151 (2026-09-05) - a render is not a destination write

New state: rendering. ArchivalStateValue gains a value, and it is a split rather than an addition: archiving used to mean either leg was in flight, so an edition being re-rendered into the protocol's own store reported that it was being written to Arweave. Now rendering means a render into the landing store and archiving means a write to a permanent destination - which is the only state whose copy may name one. A client switching on the union should treat rendering the way it treats a first render: work in progress, nothing permanent happening yet.

Changed: Storage is its own page. The hub serves it at /storage, in the You section; /arweave redirects there. Naming the page after one destination stopped being right the moment there was going to be more than one, and the page is now split the same way: what you have stored and which editions are written nowhere permanent sit at the top, and everything shaped like one destination - a byte allowance, credits in winc, pages of room - sits under that destination's heading.

sdk 0.14.130 + ui 0.7.150 (2026-09-05) - what the first Save As walk found

Fixed: a permanent manifest could point at the landing store. The manifest a Save As freezes no longer carries the og card. Og lives on the landing store, and a permanent document pointing there points at a link that dies with the account - which is the thing detaching an edition exists to prevent. The severance check that should have caught it knew risingtide.app, localhost and vercel.app but not the blob host, so it now refuses vercel-storage.com too.

Fixed: a save that was still settling was reported as a failure. A data item is served by the bundler's caches immediately and indexed by the canonical gateway minutes to an hour later, so the manifest leg no longer re-probes an image the receipts leg already fetched, hash-checked and owner-checked, and it falls back to the other named caches for anything it does verify. When a client does exhaust its retries, the run keeps its status instead of being marked FAILED, so the next attempt resumes it rather than paying to upload the same bytes twice. The chip says the copy is still settling rather than that the save failed.

Fixed: every uploaded item is recorded. A verified data item now gets its own blob row, referenced under its destination (product_instance:ARWEAVE) so it sits beside the landing copy rather than evicting it. Before this the protocol had paid for permanent items it held no record of. The manifest blob also carries the sha256 of the bytes that were verified, so it is content-addressed like everything else in the store.

Fixed: Save As... stays available after an edition has been written. Only a locked pointer is permanent; an unlocked one can be re-pointed, and gating the offer on never-having-been-saved made an owner's first save their last. Fixed: the chip no longer keeps showing work in flight after the server says it finished, and a Save again now attributes its render to the resave rather than to the app the edition was created in.

Changed: no wallet prompt on a save. Every other write in the protocol is signed without one, and Save As was raising Privy's embedded-wallet confirmation because the client set no showWalletUIs. It is off now, across every app.

New on rtAdmin.storage.getStats(): sweepableCount and permanentUnreferencedCount split the old orphanCount by what can be done about it. A superseded copy on the landing store is reclaimable space; a superseded copy on Arweave cannot be deleted by anyone and the row is the record that the protocol paid for it.

sdk 0.14.129 + ui 0.7.149 (2026-09-05) - Save As, under the owner's key

New: the storage wallet. rtClient.users.enableStorage(address) records the Privy embedded EVM wallet an app creates in the user's session (POST /api/users/me/storage-wallet). The server confirms through Privy that the address is linked to the caller before persisting it, never creates or changes the wallet itself, answers a second call with the recorded address and writes nothing, and refuses a bring-your-own-wallet account with a 409 - that account signs saves with the wallet it already holds. users.getStorage() reads the posture (GET /api/users/me/storage): the wallet, the remaining free allowance as bytes and as "about N pages", credits in winc, the automatic destination, and how many owned editions are saved but written nowhere permanent. A null allowance or balance means the bundler could not be read - it is never zero and never a stale number. users.setSaveAsDefault(&apos;NONE&apos; | &apos;ARWEAVE&apos;) (PATCH) turns automatic Save As on; ARWEAVE needs a storage wallet. New unions SaveAsDestination and SaveAsRunStatus (both mirrored from Prisma and guarded), plus SaveAsDestinationSchema and UserStoragePostureSchema in @rising-tide/sdk/schemas.

New: Save As in three legs, rtClient.productInstances.planSaveAs / postSaveAsReceipts / postSaveAsManifest over POST /api/product-instances/{id}/save-as/{plan,receipts,manifest}. The plan returns every artifact image (never og) with its landing URL, bytes and tags, the key the items must be signed with, the bundler's per-item cap, and the wallet's allowance so a client routes to a top-up before uploading. The client signs and uploads under the owner's key; the server verifies every receipt before recording anything (fetched from a named cache, byte length and sha256 checked against the saved copy, signer checked once the gateway indexes it) and answers a 422 with details.transient when the cache has not served the item yet. Once every image is written the receipts result carries the manifest to sign next; the manifest leg verifies it the way a detach would, records it as the edition's permanent manifest, and enqueues the auto-detach. SaveAsRun rows record each run and its failure class; rtAdmin.storage.getPosture(address?) reads them with the landing-store usage, bytes per create, the saved-but-unwritten backlog, the protocol wallet's remaining allowance (legacy), and any address's allowance by lookup.

Renamed: productInstances.requestArchive -> requestResave over POST /api/product-instances/{id}/resave, and the token parameter accepts null so a bring-your-own-wallet session sends the token already set on the client. The route re-renders into the saved copy; writing anywhere permanent is a Save As.

ui: new @rising-tide/ui/storage entry. SaveAsButton (the per-edition chip and action, in the storage vocabulary: Saved / Save As... / Saved to Arweave / Save again), useAutoSaveAs (runs the flow silently for custodial users who chose a destination, once per edition per six-hour window; a no-op for a wallet session), useStoragePosture, useEnableStorage (creates the Privy wallet, then records it), usePrivyStorageSigner, runSaveAs, and the signer builders. @ardrive/turbo-sdk and @dha-team/arbundles are optional peers loaded lazily when a save starts - an app that mounts the button installs them; the hub does, the client apps do not.

sdk 0.14.128 + ui 0.7.146 (2026-09-05) - saved by default

New: saved on ArchivalStateValue. An edition whose render has landed in the protocol's store, and whose owner has not acted, reads saved. It sits between live and archiving and is the resting state of every edition; live now means no render has landed yet. A client that switches on archival.state needs an arm for it, or the item falls through to whatever its last arm says. ArchivalStateSchema carries the value.

Changed: a save lands in the protocol's store and writes no manifest. The render path no longer touches Arweave. Every rendered image lands in Blob under a network-prefixed key, and the render job is complete when those rows exist. A manifest is an Arweave document whose one job is to point at Arweave ids, so it belongs to the owner's Save As and is no longer written at save. The admin save routes (POST /api/admin/{pages,editions,events,reservations}/{id}/save-to-arweave) return images and no manifest.

Changed: the token metadata image. The document served at a contract's token URI carries the saved portrait's URL once a render has landed, and the live thumbnail route before then. The saved bytes are the served bytes.

Changed: the terminal job-failure alert names the leg. jobs.render.failed is retired; jobs.rasterize.failed and jobs.blob.failed replace it, beside the unchanged jobs.detach.failed. The admin health document's jobFailures gains rasterize and blob beside render.

sdk 0.14.127 + ui 0.7.145 (2026-09-04) - an invite to give announces itself

New: invite.granted on NotificationKind, with NotificationItem.granted. An invite to give arriving in a holder's hand - a rung's payload, the weekly drip, or an operator's hand-off - is now a notification. granted.line is the sentence, written by the protocol (Fifth day earned you an invite to give.); granted.source says which of the three it was, and achievementSlug / achievementName name the rung when one paid it. INVITE_ASSIGNED writes a RECIPIENT participant row for the holder at the emit, and none when the actor is the holder. A client inbox that switches on kind needs a branch for it, or the item falls through to whatever its last arm says. NotificationInviteGrantedSchema ships beside the type.

Changed: POST /api/events accepts sourceAppId, the spelling CreateEventInput declares, as well as appId. Until this release every create the SDK could express answered 400, and no event contract had reached a chain.

Changed: the waitlist conversion defaults. waitlist.approve with no options mints an invite carrying the cohort role and Bottle Drop - the defaults grantInvite already applies - so a claimer can create what they were let in to create. roleId: null stays an explicit choice to confer none.

Docs: defaultDisplayName / defaultBio removed from the provider pages (the fields left ProtocolUserConfig in ui 0.7.128 and 0.7.132); the create-to-earn calibration names the invite's real 25 STX; the invite pages say creator and holder.

sdk 0.14.126 + ui 0.7.144 (2026-09-04) - achievements carry art

New: artUrl on AchievementAward and LadderRung. A picture for the award, absolute and nullable.

Absolute because the shelf renders in four client apps on four origins: a path would resolve against whichever one asked and 404 in the other three, which makes the protocol art a hub-only feature by accident. An award with no art of its own falls back to the icon of the app the rung is walked in - the app is what the rung is about - and a protocol-wide rung with neither is null, which a surface renders as no image rather than a broken one.

Copy: the Bottle Drop rung is a reply, not a note. note-1 reads "First reply" and its lines say leave a reply. Bottle Drop calls what comes back a reply, and this copy is served into that app. The slug is unchanged - it is a stored key on the awards and counters tables.

sdk 0.14.125 + ui 0.7.143 (2026-09-04) - an invite has a creator, not an owner

Breaking: Invite.ownerId is now Invite.creatorId, and Invite.owner is Invite.creator. InvitePreview.owner moves with them, and the admin-only list override is ?creatorId= rather than ?ownerId=. InviteListParams.scope takes 'created' in place of 'owned'; 'held' is unchanged.

Three parties can be attached to one invite and only one of them is called an owner in the schema, which made owner read as "who has it" - the question holderUserId answers. The column is the accountability row: who minted it. Every other name on the model already says what the party did.

No aliases. The only caller of the query param is the operator panel in this repo, and a field answering to two names is the drift this rename exists to end.

ui 0.7.142 (2026-09-04) - a tab can carry a count

New: AppShellTab.badge. { text, label } or null - a count riding on a bottom-tab icon.

text is a string rather than a number on purpose. The truthful badge is often "9+", and "more than nine" is not a number: an app reading a capped count knows the total is a floor and the shell cannot, so formatting belongs to the caller and placement belongs to the shell. label is what a screen reader says instead, and it is required rather than optional because a bare "3" beside a tab name is not a sentence.

The badge anchors to the icon rather than the tab, so a count appearing does not grow the tab and shift every other label along the bar.

sdk 0.14.124 + ui 0.7.140 (2026-09-04) - the feed can say somebody joined

New kind: invite.claimed. NotificationKind gains a fifth value, and NotificationItem gains invite: NotificationInvite | null - { displayName, address }, both null when the account behind the claim is gone. It is set only on this kind, the way achievement is set only on achievement.unlocked.

This is the one notification addressed to a person for what somebody ELSE did with something they gave away, which is why it does not reuse buyer: nobody bought anything, and a person-shaped field named for a purchase would be a lie in the type.

The write side moved with it. A claim now indexes both parties - the claimer as ACTOR, the inviter as COUNTERPARTY - so it can be found by the address of the person it is about. Before this, INVITE_CLAIMED wrote no participants at all, and the feed reads participants and nothing else, so "somebody you invited joined" was not a missing page but a missing index entry. Claims recorded before the change were back-filled.

The inviter is the HOLDER, falling back to the creator - the same rule InvitePreview.invitedBy uses, so the notification credits whoever the claim screen just named.

sdk 0.14.123 + ui 0.7.139 (2026-09-04) - a notification says who did it

New: NotificationItem.buyer. A NotificationBuyer - { address, displayName, isProtocol } - or null when nobody acted, which is a delivery (it rides the product's own deploy transaction) or an achievement (the protocol announces it; nobody performs it).

The feed carried a payment and no payer, so every client filled the gap with "someone". On a newcomer's FIRST earning that is always false: the protocol buys the first copy of what a new person makes, so they earn before anyone has found them. Two apps invented a stranger independently, which is what an anonymous buyer guarantees. isProtocol is how an app says what actually happened, and displayName resolves to the protocol's own persona in that case rather than to the operator account behind it.

New: InvitePreview.invitedBy. Who to credit for an invite - the HOLDER, falling back to the creator when nobody holds it. owner stays, and is now documented as provenance.

An invite the ladder pays out is MINTED by the protocol and HELD by the person who earned it, so a client crediting owner credits the house on every earned invite - including on the earner's own list, where the invite they worked for was labelled as somebody else's. The join is resolved server-side because "who invited me" is one fact, and each surface that computed it got it wrong. Prefer invitedBy over owner wherever a person is named.

sdk 0.14.122 + ui 0.7.138 (2026-09-03) - events get a public hook surface

New in @rising-tide/ui/react: nine event-domain hooks. useEvents, useEvent, useCreateEvent, useUpdateEvent, useDeleteEvent, useLocations, useEventTypes, useTicketStyles, useCurrencyTypes. They read the client from context like every other hook in the package, and the mutations invalidate both the list and the row they touched.

Until now the only React path to events was through @rising-tide/sdk/admin, whose hooks take a RisingTideAdmin as an argument - so an app building on events hand-rolled its own, which is exactly what rt-events did. Those admin duplicates are removed in this version; the nine names above are now the one place each lives.

New on the core client: rtClient.ticketStyles and rtClient.currencyTypes. Both were mounted only on the admin client, and both of their GET routes are ordinary app-key reads - so a client app that renders a ticket, or shows what currencies a product may be priced in, had no supported way to ask. Mounting them widens no privilege; the routes are unchanged.

Renamed: the cross-app listing hooks. useGetListedApps becomes useListedApps, useListPageOnApps becomes useListProductOnApps, and useRemoveAppListing becomes useRemoveProductListing; their pageId argument becomes productId. They act on any smart product and always did - the page naming predated the smart-product model.

Fixed with that rename: the two listing mutations invalidated nothing a hook reads. They cleared ['pages'] and ['page', id], while the substrate hooks key on ['smartProducts'] and ['smartProduct', id], so listing a product on an app left every open view of that product stale. Both now invalidate the product keys as well as the listing key.

Removed: usePageEditions. It made the identical call as useProductInstances (smartProducts.getInstances) under a different query key, so the two held separate cache entries for one dataset and a mutation could only ever refresh one of them. Use useProductInstances.

Removed from @rising-tide/ui/react: the chainhook hooks. useChainhookStatus, useRegisterChainhook, useRegisterChainhookV2 and useRegisterEditionTracking are no longer exported - chainhooks are registered by the protocol as a product deploys, not by a client app. useRegisterChainhook also duplicated its own V2 exactly.

Removed from @rising-tide/sdk/admin: six operator hooks with no caller. useCacheStats, useDetailedHealthCheck, useRole, useUserRoles, useLinkType, useLinkTypes. rt-admin reimplemented each locally some time ago, and the copies left behind still polled with setInterval where the live ones use the visibility-aware helper.

sdk 0.14.121 + ui 0.7.137 (2026-09-03) - two fields nothing could read

Removed: WaitlistQueueRow.suggestedGrantAppIds. It carried the apps an approval should open, derived from what the applicant ticked on the signup form. The achievement ladder is what opens apps now, so an approval grants Bottle Drop whatever the interests suggested - an invite that opens Grammata and On The Record up front leaves the rungs that pay for those doors paying nothing. The console stopped reading the field when the ladder shipped, and a prefill labelled suggested grants is an invitation to undo the ladder by hand, so it is deleted rather than re-labelled. suggestedMessage stays: a builder still gets pointed at the developer docs.

Removed: NotificationReadResult.persisted. It meant this address has no account, so there is nowhere to record a read, and the route can no longer answer that - authenticateUser refuses an address it finds no user for, so a 200 always names a watermark that was written. The FEED still serves an account-less address; moving the watermark is person-keyed and needs a person.

Both were fields that could still be read and could no longer be true, which is worse than a missing one: the type documented behaviour the server had stopped having.

conformance 0.9.4 (2026-09-03) - a marker split across a line wrap is still a marker

Fixed: comments-are-point-in-time matched each line of a comment separately. A comment is wrapped to the editor's column and the phrase it carries does not respect that wrap, so The counts that used / to sit here was one sentence, one marker, and no violation. Every marker with a space in it had the same hole: no longer, was removed, operator ruling.

Markers are now matched against the whole comment, with its /**, * and // furniture removed and its whitespace collapsed - and the reported excerpt is the sentence around the hit rather than a raw line. One comment reports one hit.

Scored against every consumer before shipping: two comments in ten apps newly fail, both real narration, both fixed in the same roll. The new extractCommentBlocks and normaliseComment are exported for a guard that mirrors this grammar.

SDK unchanged at 0.14.120, ui at 0.7.136.

ui 0.7.136 (2026-09-03) - arrival is Google or X

Changed: DEFAULT_LOGIN_METHODS drops passkey. RisingTideProviders now offers ["google", "twitter"] where it offered ["passkey", "google", "twitter"]. An app that wants something else still passes its own loginMethods; the default is what every app in the estate uses today.

The reason is the invite, not a preference: an invite is the only way in, and the claim works with Google and X. A method that signs somebody in but cannot claim leaves them holding an account with nothing in it. Narrowing the client array asks for less than the Privy dashboard allows - it cannot make a method impossible, so the dashboard is the other half of turning one off.

SDK unchanged at 0.14.120.

sdk 0.14.120 + ui 0.7.135 (2026-09-02) - the ladder as a map

New: users.getLadder() reads GET /api/achievements/ladder and returns a Ladder: rungs, the onramp in the order it is climbed, and returns, the awards earned by coming back rather than by climbing. Each LadderRung carries { slug, name, family, requirement, need, pays, appId, appName, appUrl, renewsEvery }. It takes no address and no bearer token - the rungs are the same for everybody, which makes this the one achievements shape a signed-out visitor can read.

It is the companion to getAchievements, not a replacement: the shelf is per address and its next is deliberately one rung, so a person climbing sees the step under their foot and never the shape above it. Join the two on slug - awards for what is behind the viewer, next.slug for where they stand - and keep the instruction on one surface; the map is a different job.

The copy is future-facing and count-free, because it is written for somebody who has not reached the rung: requirement says what to do, and pays says what it opens, or is null when the record of it is the whole reward. pays names the app a door opens onto rather than the one the rung is walked in, and a rung whose door has no App row to name reports null instead of a half-sentence.

sdk 0.14.119 + ui 0.7.134 (2026-09-02) - the shelf and the next rung

New: users.getAchievements(stxAddress, { appId? }) reads GET /api/users/address/{addr}/achievements and returns an AchievementShelf: the awards an address has earned (newest first, each with its line already resolved and the app it happened in) plus next, exactly one next rung with a live count against its requirement - { slug, line, n, of, appId, appName, appUrl, graduated } - or the graduated state once the onramp is complete. next is the person's own view only: it is included when the request carries the bearer token for that address, and a stranger (or a call with no token) gets the awards and next: null. So a null next on your own address means the token was not on the wire, not a finished ladder; finished is next.graduated === true, with its own closing line and nothing to open. Nothing earned is an empty awards, never a 404. Pass appId for the app your surface belongs to: a door that has just opened reads as news everywhere except in the app behind it, where the instruction for the next rung is the useful sentence. AchievementShelfSchema, NextRungSchema and AchievementAwardSchema ship in @rising-tide/sdk/schemas.

New in @rising-tide/ui/react: useNextRung(sdk, { appId? }) - the one source for the next-rung line in every app. Reads the shelf for the signed-in address once the auth token is on the wire, returns { next, shelf, loading, refetch }, and exports achievementShelfKey so an app can invalidate the line after the act it asked for. Render next.line where your primary action is, and when next.appId is not your app, make next.appUrl the action.

NotificationAchievement.opened gains appUrl. The feed now resolves the opened app's launch URL at read time, so an inbox row can be the door rather than a sentence about it.

sdk 0.14.118 + ui 0.7.133 (2026-09-02) - the rungs pay

NotificationKind gains achievement.unlocked, and NotificationItem gains achievement. The protocol now announces an achievement the moment its award is written: one event per award, addressed to the earner, arriving in the notification feed as its own item. The item carries achievement: { slug, name, line, earnedAt, opened } - line is the sentence to show with its placeholders already resolved (what the seed paid, for instance), and opened names the app the rung opened when it opened one, so a client can render the door beside the line. achievement is null on every other kind, and subject and amountSbtc are null on this one. A client that switches on kind with a fallthrough will render the new kind as its fallthrough until it adds a branch; Bottle Drop and On the Record add theirs in this roll. NotificationAchievementSchema ships beside it in @rising-tide/sdk/schemas.

ProtocolEventType gains ACHIEVEMENT_UNLOCKED. Off-chain, PROTOCOL actor, txId null, with one RECIPIENT participant. Its occurredAt is the announcement; payload.earnedAt is the award.

AppAccessSource gains ACHIEVEMENT. A door the ladder opened is a grant row with this source, written by the protocol and revocable like any other.

Removed: ChallengeType and ChallengeStatus. Neither had a model, a service or a route behind it; the SDK carried the unions only because the Prisma enums existed, and both are dropped in the same change. Nothing that builds today imports them.

ui 0.7.133 is lockstep only.

sdk 0.14.117 + ui 0.7.132 (2026-08-31) - a display step, and dead config removed

Breaking, one field: ProtocolUserConfig.defaultBio is gone. It was required config that every consumer set to the empty string, so it stated nothing and the two reads behind it wrote an empty bio rather than leaving one absent. Remove defaultBio from the auth block of your RisingTideProvidersConfig; an absent bio is now simply absent. Same shape as the defaultDisplayName removal in 0.7.128.

@rising-tide/ui/theme ships .rt-display, and --text-6xl is mapped. --rt-text-6xl (88px) has been in the token layer with no Tailwind utility behind it; text-6xl now resolves. The class is the front-door display treatment: mono, weight 500, 88px, stepping down to 48px below 768px. It is a class rather than a utility because the heading element rules are unlayered, so a size utility on an h1 renders nothing while a class outranks the element rule on specificity. Marketing surfaces opt in; every console surface keeps the h1 default.

ThemeToggle's size prop works. It was in the props interface and never read, so sm, md and lg all rendered a hardcoded 40x40 button. The three sizes are now real, carry the primitives' 44px touch floor, and the component styles itself from the token set rather than arriving as an unstyled button. It also no longer seeds from the OS colour scheme: an absent stored preference resolves to dark, which is the estate's base declaration.

Removed, not deprecated: StatusBadgeProps. The interface described a generic badge shell that was never implemented and was never exported from the /react barrel, so nothing can break. The five typed badges and the class helpers are unchanged.

sdk 0.14.117 is a comment repair only, distributed to keep the two packages in lockstep.

conformance stays at 0.9.3. rt-landing and rt-admin join it in this roll, so every app that ships UI now runs the checks, but the package itself is unchanged.

sdk 0.14.116 + ui 0.7.131 (2026-08-31) - the operator half of the waitlist

rtClient.waitlist gains the admin surface: list, approve, decline, annotate, requeue and remove. All six are gated on VIEW_ADMIN_PANEL server-side and ride the operator's Bearer token, not an app key.

approve is not idempotent, and that is the design. A second call on the same request is refused with NOT_PENDING and mints nothing - two clicks must not mean two funded invites. It goes through the ordinary mint path unchanged, so a role that may not mint gets the same refusal it would get from the invites console, and an InviteError is passed through with its own status rather than flattened. The invite it creates expires in 90 days rather than the usual 30, because delivery is pull-only and a month is not long enough for someone who was never told.

list answers a cohort question, not a page of rows. WaitlistQueue carries buckets - pending counts crossed by interest and experience - alongside the rows, so "five writers with little to no crypto experience" is one query. Each row also ships suggestedGrantAppIds and suggestedMessage, derived from what the person said they came for, so a queue and an approve dialog cannot disagree about what an interest implies. A builder's suggestion is deliberately empty: they need the docs and the SDK, not an app grant.

WaitlistQueueRow.alreadyUserId is advisory only. It matches a self-reported profile field, which may be trusted to refuse an action and never to grant one, so it helps an operator not double-invite someone and decides nothing. invite.status on the same row is how the pull model's failure rate becomes measurable at all: SENT and aging means approved-and-lost-so-far, EXPIRED means lost for good, CLAIMED is the only success.

sdk 0.14.115 + ui 0.7.130 (2026-08-31) - a stranger can ask to be let in

rtClient.waitlist exists: signup() and status(). Both are reachable with only an app API key, which is the point - the person asking has no account, and creating one for them before they are let in is the dead end this surface removes. Neither method mints, funds, or grants anything; an operator converts a request into an invite by hand through the ordinary path.

A duplicate signup is indistinguishable from a new one. Same 200, same body, same statusToken. Do not branch on new-vs-duplicate - you cannot see it, deliberately, because a different response would let anyone test whether a given person had applied. A contact that does not parse still returns a normal 400.

WaitlistStatus.contactRaw is the address they typed, and only this response may name it. InvitePreview.boundTo carries the binding TYPE only, so a claim page can say "sign in with Google" but never which Google account - a loop for anyone with two of them. The status token scopes this payload to the person who filled in the form, so echoing their own submission back leaks nothing. An unknown token resolves to { status: 'UNKNOWN' }, never a 404.

InviteCodeStep now offers a way out. Every app in the estate rendered an invite-code box with no way to get a code; it carries a "No code? Ask for one." link to the waitlist, as does ComingSoonPage. The href is absolute and network-aware - a relative path would resolve against whichever app rendered the dialog. waitlistJoinUrl() is exported from @rising-tide/ui/auth; NEXT_PUBLIC_WAITLIST_URL overrides the origin for a preview deployment.

sdk 0.14.114 + ui 0.7.129 + conformance 0.9.3 (2026-08-31) - a queued faucet tap has a name, and a fork gets graded

PrivySignResponse.queued exists. Both faucet routes can answer { success: true, queued: true } with no txId: the request was accepted and made durable in the protocol outbox, but another drainer holds the wallet, so nothing has broadcast yet. The field was already on the wire and absent from the type. A caller that reads a missing txId as failure is wrong for this case - a queued tap is an acceptance, and the next drainer delivers it.

ds-button-extension-contract grades a fork that declares extends as an array. The rule's opt-in gate compared extends as a string, so an app that opted into two rule sets - the form conformance 0.9.2 introduced - stopped being checked and reported not applicable with a zero exit. Measured on the estate's only real button fork: 19 passed, 1 not applicable before, 20 passed, 0 not applicable after, same app and same code. A rule that grades nothing reports success, which is the one failure a conformance run cannot show you.

The widened comment-hygiene marker ships. comments-are-point-in-time recognizes the FCR-reference form that the narrower pattern missed. Opt-in as before; it is never in recommended.

ui 0.7.128 (2026-08-30) - contact fields stop leaving the API, and a skipped name is distinguishable

email and xHandle come back null on every user read a client can make. That covers GET /api/users, /api/users/search, /api/users/address/{addr}, /api/users/address/{addr}/profile and POST /api/users/batch-lookup. The keys stay present, so UserSchema and UserProfileSchema still parse; the values are withheld. privyUserId and privyWalletId are dropped outright. GET /api/users/{id} is the one read that still answers with them, and it now requires the VIEW_ADMIN_PANEL entitlement.

Neither field is searchable, sortable or filterable any more. q and search match displayName, stxAddress, btcAddress and (on /search) bio. The hasEmail and hasXHandle filters and the email sort key are gone: a contains probe over a contact column answers the question the blanking exists to withhold.

ProtocolUserConfig.defaultDisplayName is removed, and every app shares one fallback. Someone who skips the welcome form is now named User <last four of their address> instead of a per-app persona (Castaway / Writer / Artist / Admin). displayName is the only thing user search matches on, so a flat word made every skipper the same unusable row; the suffix discloses nothing, since the address is already public. fallbackDisplayName(stxAddress) is exported from @rising-tide/ui/auth for apps that render their own empty-name fallback.

conformance 0.9.2 (2026-08-29) - comment hygiene ships as an opt-in set

comments-are-point-in-time exists, and it is never part of recommended. The rule flags dev-log narration in comments - dates, phase and spike references, was/used-to history - and it is our house style rather than a protocol fact, so a stranger's app never sees it unless that app asks. Opting in: "extends": ["rising-tide-design-system", "rising-tide-comment-hygiene"] - extends now accepts an array as well as a single name. The pending rule list is gone: a new rule ships at error after being measured against every adopting app, or enters recommended at severity warn with its promotion version stated.

sdk 0.14.113 + ui 0.7.127 (2026-08-29) - a ticket knows when its event starts

ProductInstance.product is typed SmartProduct, not SmartProductPage. The include=page join has always carried the parent whatever its domain - an EVENT-domain instance carries its event there - and the type now says so. The join also carries eventDetails for EVENT-domain parents, so a ticket row can name its start time in one request; pageDetails stays off the join on purpose (page content is heavy, and the listing is a tile surface). The domain narrowers isPage / isEvent / isPrediction are exported from the SDK root for consumers that need a specific domain's detail row.

userExists answers, or refuses to. It returns false only when the server answered "no such user" (a 404); a 429 or a server error now rethrows instead of reading as false. A swallowed 429 once routed an existing, signed-in user into the new-user welcome flow. On the ui side, ProtocolUserProvider treats a rate-limited initialization as throttled rather than signed out: the typed RateLimitError stays in context and the provider retries when the server's Retry-After window resets.

sdk 0.14.112 + ui 0.7.126 (2026-08-29) - comments state the present

No API surface changes. An estate-wide comment-hygiene pass: every comment in both packages now states a constraint or intent the current code cannot show, and the dev-process history that used to ride along (dates, phase references, what something replaced) lives in the dev logs and git where it cannot rot against the code. The same pass adds an opt-in rising-tide-comment-hygiene conformance extension set - our house style, deliberately never part of recommended - and conformance.config.json's extends now accepts an array so an app can hold several sets at once.

sdk 0.14.111 + ui 0.7.125 (2026-08-27) - the frozen cap table gets a type

SmartProduct declares contributions, and Contribution declares frozenPercentInt. Both have ridden the wire under ?include=contributions since before either type existed, so any client wanting to show a reader where their fee goes had to read them structurally at the boundary.

const res = await rtClient.smartProducts.getById(id, ['contributions'])
for (const leg of res.data?.contributions ?? []) {
  if (typeof leg.frozenPercentInt !== 'number') continue   // DRAFT, or frozen pre-column
  const sats = Math.floor((feeSats * leg.frozenPercentInt) / 100)
}

Read frozenPercentInt, never percentage. The float is what the creator asked for; the integer is what was apportioned at freeze and what the contract transfers - which is why the protocol's own payout helper throws on the float. Floor each leg independently, the way the generated Clarity does: handing one party the remainder invents an amount no transfer makes, and the leftover sat is dust.

And the fee is a floor, not a price. minFeeSbtc is a minimum that payers exceed, so instanceCount x share is not what a product earned. Read the settled ledger - payments.getUserRevenue(addr, { role: 'creator', groupBy: 'product' }).

Also in this pair:

  • UserProfileAggregate describes apps[].totals (new UserAppScopedTotals), the per-app slice that has been on the wire unnamed. Read it, not the protocol-wide totals, from anything wearing one app's brand. UserProfileAggregateSchema gained it plus four totals fields it had been silently stripping.
  • ProductTypeListParams is Record<string, never>, and StyleListParams no longer extends ListQueryParams. Both used to type a query key their route rejects, so list({ limit: 100 }) compiled clean and 400d at runtime. This can turn a consumer red - that is the point; a red line is a call that was already failing.
  • The testnet strip carries one line on a phone. Below sm it shows the first sentence of its message; TestnetBanner takes an optional shortMessage to override that split. The paused (idle-sleep) message is never shortened.
  • ConnectWalletButton no longer ships a fontWeight: 700, above the estate's cap of 600.

sdk 0.14.110 + ui 0.7.124 (2026-08-26) - the social-card render becomes reachable

GET /api/product-images accepts prefer=og, and productImages.list() takes { prefer: 'og' }. ProductImageRole gains IMAGE_OG.

The render pipeline has always produced a 2400x1260 IMAGE_OG for every product and edition - the social-card aspect, drawn for that purpose and archived permanently alongside the portrait and landscape renders. Nothing had ever read it. The route's only preference list was the display one (IMAGE_PORTRAIT, IMAGE_SQUARE, IMAGE_LANDSCAPE), so there was no way to ask, and three client apps pointed their og:image at an rt-api route that does not exist. Every link shared from them carried a 404 as its preview image.

const res = await rtClient.productImages.list('smart_product', [id], { prefer: 'og' })
const url = res.data?.images?.[String(id)]?.url   // undefined while the render is pending

Three things to know before using it:

  • prefer: 'og' does NOT fall back to a display render. A 0.67 portrait in a 1.91 card slot is cropped to a band across its middle, which is worse than your own default card. Absent means the render has not finished - substitute a correctly-shaped static card of your own.
  • The default is unchanged. Omitting prefer returns exactly what it always did, so every grid in the estate is unaffected.
  • An edition carries its own card. Pass product_instance for a shared edition rather than reusing the parent product's image - the thing that was shared is the thing that should be pictured.

conformance 0.9.1 (2026-08-26) - the count the guard writes reaches zero

The credits-is-not-a-unit rule gains the last matcher variants the estate-wide sweep exposed, and with them all eight conformance consumers pass with zero allow rows. The work of this distribution was mostly in the apps (rt-events, rt-studio, rt-landing and rt-sample each retired their remaining credits surface); the package change is small and exists so the rule sees what those fixes see.

ui 0.7.123 (2026-08-26) - clients read the create fee instead of carrying it

ui only. The SDK is unchanged at 0.14.109, so all ten apps stay on one tier.

useProtocolUser() now exposes createFeeMicro - App.createFeeMicro from GET /api/apps/{id}/config, parsed to a bigint. 0.14.109 made the protocol able to state what creating costs; this is the half that makes clients read it.

Null means "no server answer yet" - the config is still loading, or no appId is configured. It is not zero and must never be rendered as a price. Fall back to your own compiled default: a momentary 0 renders creating as free and opens an affordability gate on an empty wallet.

Two things worth copying from the apps that adopted it:

  • The fee is the protocol's; the gas buffer is yours. A deploy burns both, so gate affordability on fee + gas. But send the bare fee as deployFeeSats - that amount is frozen into the contract and pinned by a deny-mode post-condition, so padding it with a gas buffer overcharges the author by exactly that buffer.
  • Read it through whatever abstraction your app already uses for auth state. useProtocolUser throws outside the real provider stack, so an app with a mock/offline provider branch must not call it from a shared hook - that works in production and breaks local dev, preview and build validation, and tsc cannot see it.

A malformed value degrades to null rather than throwing, since apps.getConfig casts its response rather than validating it and BigInt("x") would throw during render of the provider every app mounts.

sdk 0.14.109 + ui 0.7.121 (2026-08-26) - creating is priced in the marketplace's own token

Credits are retired. Creating is priced in whatever the marketplace charges - STX on Stacks - and collecting stays sBTC, in sats. You pay the venue in the venue's currency, and you get paid in money.

The short reason: a credit is scrip, and scrip needs a trusted issuer holding a reserve and promising to honour it. A protocol meant to be forked and run by other people cannot sit at the centre of that promise. Adding a second marketplace under credits would have meant a float, an FX pair and a margin policy per chain; under native pricing it is an adapter.

GET /api/apps/{id}/config gains createFeeMicro - what creating costs in that app, in the marketplace's base unit, as a numeric string (it is a BigInt server-side). Named for the unit rather than the token, because the token is a property of the marketplace.

Before this, each client carried the number in its own env var, so the protocol could not answer "what does creating cost here" - and App held deployFeeSharePercentage, a share of a fee it did not itself know. Read it from config; keep your own gas buffer if you need one.

App.acceptedCurrencies now defaults to ["SBTC"] and every app ships that way. It was [SBTC, STX], and the STX half was a door nobody used: all 24 mint legs ever settled were sBTC. The two currency fields answer different questions and are deliberately separate - acceptedCurrencies is what a COLLECTOR may pay in, createFeeMicro is what a CREATOR pays to publish.

Nothing on chain changed, no contract was redeployed, and no ledger row moved. This is vocabulary, one additive column, and a default.

conformance 0.9.0 (2026-08-26) - credits stop being a unit anywhere

stx-not-a-spending-unit becomes credits-is-not-a-unit - the rule number stays so the history is traceable, and the rule inverts with the currency ruling: creating is priced in the marketplace's own token, so rendering STX is now correct and rendering "credits" is the defect. The new matcher is deliberately the widest in the package: every other rule discriminates a defect from a correct use of the same token, but credits has no correct use left to protect, so it matches the word rather than a shape. Its predecessor matched string literals and never a currency=&quot;STX&quot; prop - it scored rt-sample clean while the app rendered the exact string the rule existed to catch. The four positive controls are real bracket-blitz lines, and they are negative controls now: defects under the old model, correct under this one.

ui 0.7.120 (2026-08-25) - a sticker fills its placement box, whatever its markup

ui only. The SDK is unchanged at 0.14.108, and this release keeps pinning it, so all ten apps stay on one tier.

StickerEditor sizes a wrapper from sticker.width * scale and injects the sticker's SVG raw. An SVG carrying only a viewBox fills that wrapper. One that declares its own width and height renders at that size and ignores it - so the scale slider moved an empty frame while the art sat still, and rotation pivoted around a centre the art no longer occupied.

The editor now forces the injected SVG to 100% in both axes, the way the sticker picker already did. An SVG with no viewBox is left alone, because there is nothing for a percentage box to scale into and the same two attributes would collapse it rather than fit it.

Update if you render StickerEditor. No API change, no props change - drop in the new tarball.

Server-side compositing never had the bug: composeStickerLayer adds width/height only when they are absent, so both shapes composited identically. The defect was only ever visible while placing.

If you ingest stickers through this protocol you are covered twice over - the API strips intrinsic sizing from a sticker's root <svg> on create and update, since the viewBox is required anyway and is what the stored dimensions are derived from.

sdk 0.14.108 + ui 0.7.119 (2026-08-25) - a series holds assets, not just templates

A style set could not exist without at least one product type, because that is what a set's domain is derived from. So an artist who wanted to group stickers under one brand had nowhere to put them: a set with no template was invisible to every list query in the protocol, including the one its own maker reads.

rtClient.styleSets.listByArtist(artistUserId, opts) answers everything this artist has made - their series, and every sticker they own whether it belongs to a series or not. It asks no domain question, which is the whole point:

const { data } = await rtClient.styleSets.listByArtist(artistId, {
  includeUnapproved: true, // owner or operator only
  includeInactive: true,
  includeSvg: true,
});

data.series;   // each with _count { styles, productTypes, stickers }
data.stickers; // each with styleSetId - null when standalone

Approved-and-active is public. Drafts and deactivated work need the artist themselves or an operator holding MANAGE_TEMPLATES.

createStyleSet accepts productTypeSlug: null for a series that holds no template yet. Omitting the field still takes the server default, so nothing existing changes. styles[] needs a product type to belong to and may not accompany null.

Stickers stay global in use. A creator can apply any approved sticker to any page regardless of which series their page's template came from. Membership is provenance and portfolio, never a selection scope - which is why no client app changes and no settlement path moves.

Two things worth knowing if you consume the catalog:

  • listStyleSets({ productTypeSlug }) is unchanged and still correct. A series with no template has no StyleSetProductType row, so it is already excluded - a template picker keeps offering only sets that can style the product type in front of it.
  • A fetch by id no longer refuses a series with no product type. The domain check exists to keep an event set out of a page route; a set in no domain has nothing to mismatch. A set whose templates are all another domain is still refused.

ui 0.7.118 (2026-08-24) - nothing the package ships renders below the floor

ui only. The SDK is unchanged at 0.14.107, and this release keeps pinning it, so all ten apps stay on one tier.

Seven sites in @rising-tide/ui rendered at 10px - one step below the 11px theme.css calls "the floor, and the phone floor". They are lifted to the floor. Nothing else changes: no type surface, no API, no layout beyond the one-pixel lift.

wherewhat was small
theme.css .rt-labelthe shared eyebrow, reaching every app that imports the theme
auth AuthenticationChoice"Welcome to" and the or divider - the sign-in screen in all ten apps
shell FootBarsync state, block heights, the SHA, the version pins
shell SubNavItemthe count beside a nav label
shell RailAppStorethe rail state caption and its toggle

Update if you render any Rising Tide chrome or the auth dialog. Purely visual - drop in the new tarball, no code change.

The interesting part is why it lasted: a size has five spellings here (a raw token, a quoted literal, a bare number, a Tailwind arbitrary value, and a CSS rule), and every guard in the estate scanned an app's own src/. None of them could see a package. ui-package-floor.test.ts now scans this one, stylesheets included.

sdk 0.14.107 + ui 0.7.116 (2026-08-24) - show the thing, and let the platform open it

rtClient.productImages resolves the permanent artifact for a batch of products or editions - the same PNG the token URI advertises. See rtClient.productImages for why it is the right source for a grid and the on-demand SVG thumbnail is not. An entity whose render has not completed is absent from the reply rather than null, so a caller can tell "still being made" from "broken".

useAppLaunchTarget (from @rising-tide/ui/react) decides how a link to another app opens. Spread it onto the anchor; never hardcode a target:

const launchTarget = useAppLaunchTarget();
<a href={app.frontendUrl} {...launchTarget}>{name}</a>

There is no API that answers "is that other app installed" - the only readable signal is whether this app is running installed, and that is a property of the browser at the moment of the tap. Running installed, the link navigates same-tab, which is the only form the navigation-capturing algorithm will route into the other installed app, and the platform keeps the launching window behind it. In a browser tab it opens a new tab, because there is no capture to protect and a same-tab hop costs the launcher that sent you. Either way the surface you launched from is still there to come back to.

@rising-tide/conformance 0.7.0 replaces registry-app-launch-stays-in-tab with registry-app-launch-defers-to-platform, which fails a launch anchor that hardcodes a target in either direction - including a bare same-tab anchor. All six wired apps pass.

conformance 0.8.0 (2026-08-24) - the money rules ship narrowed, on purpose

Two rules from the hub money-teaching pass: sbtc-usd-is-secondary (a USD figure may gloss a sats figure, never replace it) and stx-not-a-spending-unit. Both ship NARROWER than first drafted: the proximity form of the second scored one true positive against eight false, so the shipped form matches the defect shape rather than the neighborhood. The narrowed rule still finds fourteen real defects, all in bracket-blitz behind an allow row with its reason. (This is the rule 0.9.0 later inverts wholesale when credits are retired.)

conformance 0.7.0 (2026-08-24) - the launch rule reverses, and it is a real reversal

registry-app-launch-stays-in-tab is replaced. Shipped the day before: an app launch stays in this tab. The ruling a day later: clicking an app should leave the hub open behind it, like a launchpad. Both are right about different browsers, and neither is decidable in source - there is no API that answers "is that other app installed", so useAppLaunchTarget reads the one signal that exists (whether THIS app runs installed) and lets the platform decide how the launch opens. The rule now checks that app launches go through that hook. Rode the sdk 0.14.107 + ui 0.7.116 distribution above, to the six apps that wire conformance.

sdk 0.14.106 + ui 0.7.115 (2026-08-24) - the app registry says what an item is called

App gains itemNoun and itemNounPlural, and homePath gains one meaning.

homePath is the per-item deep link template: a path carrying one {id} token that takes a SmartProduct id, as in /letter/{id}. It used to carry two meanings - some rows held a person path, others an item path - and the one renderer substituted a product id into whichever it found. Apps with no per-item surface now carry null, and a null is meant to render no link: a guessed path produces an anchor indistinguishable from a working one until somebody taps it.

itemNoun is what ONE thing from an app is called in a sentence a person reads - "a letter on On The Record". Absent means "item". It is never "product": that is the protocol's word for the mechanism, and nobody collects a mechanism.

UserAppRailEntry.app carries both nouns, so a rail entry constructed by hand needs the two new fields.

Two app-registry rules, both errors, both protocol facts rather than house style: registry-supplies-cross-app-routes (a hardcoded cross-app route is a claim about a routing table this repository cannot see) and registry-app-launch-stays-in-tab (a _blank on an app launch disables Chrome's navigation capture for exactly the people who installed the most apps). Each fires against the real pre-fix source read out of git and is silent on the correction. The launch rule collects frontendUrl aliases per FILE before looking at anchors, because the live defect read the front door into a local variable one screen above the anchor - an element-scoped matcher would have reported a clean file with the defect sitting in it. (The launch rule is reversed two versions later - see 0.7.0.)

ui 0.7.113 (2026-08-24) - feed the rule Privy's own status, not a mirror of it

Take this one. 0.7.110 through 0.7.112 all aimed at the same defect and none closed it.

0.7.112's rule was correct and still redirected signed-in visitors, because the provider fed it its own MIRROR of Privy - privyAuthenticated, which updates only when PrivyStateSync reports. That mirror starts false, so "has not reported yet" was read as "signed out", which is the exact confusion the rule exists to prevent, reintroduced one level up.

PrivyStateSync now hands up Privy's ready + authenticated verbatim via onPrivyStatus, separate from the derived onStateChange. Once ready is true, Privy's authenticated is final, so it is an answer; the derived mirror never was.

A rule cannot check what it is fed, so the wiring is guarded by a source read.

ui 0.7.112 (2026-08-24) - ask what is known, not how far the machinery got

Take this one. 0.7.110 and 0.7.111 both shipped today aiming at the same defect and neither closed it.

authResolved now asks a single question: do we have an answer about this visitor?

if (!authMethodHydrated) return false
if (selectedAuthMethod !== "privy") return true   // BYOW reads its connection synchronously
if (!privyReady) return false
if (!privyAuthenticated) return true              // signed out IS an answer
return privyStxAddress !== null                   // signed in: the address is the answer

Both earlier attempts asked how far the machinery had progressed instead, and both were wrong for the same reason. "Privy is ready" misses the Stacks address, which arrives from a network call afterwards. "Privy has reported" misses that PrivyStateSync emits intermediate reports - measured on the deployed hub, it reported an authenticated user with a null address 10ms after ready, long before the wallet call could return, because linkedAccounts had not populated yet. Counting that as a determination redirected a signed-in visitor to the app root.

A terminal wallet failure still resolves, because PrivyStateSync reports it as signed out.

ui 0.7.111 (2026-08-24) - resolution waits for the address, not just for Privy

Supersedes 0.7.110, which fixed this incompletely and shipped. Take 0.7.111.

0.7.110 derived authResolved from Privy's ready. That is a real gap and it was not the last one: authenticated also needs a Stacks address, and deriving it is a network call that runs after ready. So a second window existed with authResolved true and authenticated false, and gated deep links still landed on the app root - verified on the deployed hub with 0.7.110 live.

Resolution now means Privy has reported a determination, not merely become ready. PrivyStateSync also reports the terminal wallet-failure case, which previously latched and returned early forever - so a consumer waiting for an answer waited for good.

deriveAuthResolved is exported alongside shouldRedirectToLogin for direct testing.

ui 0.7.110 (2026-08-24) - "not signed in" and "not known yet" are different answers

ui only. The SDK is unchanged at 0.14.105; @rising-tide/ui pins the sdk tarball by filename, so a ui-only bump is safe where a sdk-only bump is not.

useProtocolUser() gains authResolved.

const { authenticated, authResolved } = useProtocolUser()

authenticated is false on the first render of every cold load, whether the visitor is signed out or simply not restored yet. isLoading does not close that gap: it tracks the protocol-user FETCH and is set false the moment there is no authenticated address to fetch for - which is precisely the un-restored state. So !isLoading && !authenticated reads as "signed out" for a signed-in visitor, for one tick, on every hard navigation.

AuthGate acted on exactly that and redirected to loginPath. The effect: deep links into gated routes landed on the app root. Measured on the deployed hub - /settings, /earnings and /pages each served their own document and then moved to / during hydration. In-app navigation was unaffected, which is why it stayed invisible: it only bit visitors arriving from outside, and /settings is deep-linked by name from every client app.

If you gate routes yourself, gate on authResolved before acting on authenticated. The rule is exported as shouldRedirectToLogin for direct testing.

AuthenticationChoice and AuthDialog also take an audience ("consumer" | "operator", default "consumer"), settable as auth.audience in the provider config. The consumer education block - earn sats, no crypto experience needed, you stay in control - shipped to every app that mounts the dialog, including the operator panel, which greeted an incident responder with an invitation to make something and earn. operator renders no education block rather than substituting operator-flavoured marketing.

sdk 0.14.105 + ui 0.7.108 (2026-08-23) - a delivery is not a collect

NotificationKind gains a third value, product.delivered.

type NotificationKind = 'product.collected' | 'payment.received' | 'product.delivered'

A product composing the auto-mint capability hands instance #1 to an addressee inside its own deploy transaction. That is a real mint and it reaches the creator's feed - but nobody collected it, so a client told only product.collected renders "someone collected your work" about an act no person performed.

Nothing else in the item could tell them apart. amountSbtc is null for a fee-free auto-mint AND for a collect whose payment legs missed the viewer; subject carries no instance number; no participant role differs. The distinction is not derivable client-side, which is why it had to become a value rather than a convention.

The server decides it by matching the mint's transaction against the product's own deploy transaction - the one signal that cannot be mistaken for something else. An instance number of 1 is an ordinary collect on any product without auto-mint.

Existing clients keep working. product.delivered only ever replaces a product.collected that was already being shown, so a client that has not been updated renders the same string it rendered before; one that has can say something true instead.

sdk 0.14.104 + ui 0.7.107 (2026-08-23) - what happened while you were away

The notification feed. Until now the protocol could answer what have I done and what am I owed in total, but nothing anywhere could answer what happened to me. Both existing reads aggregate, and aggregation is exactly what discards the individual events a notification needs.

const { data } = await rtClient.users.getNotifications(address, { appId: 10, limit: 30 })
data.items[0]
// { kind: 'product.collected', subject: { title: 'The last warm evening' },
//   amountSbtc: '400', unread: true, occurredAt: '...' }

await rtClient.users.markNotificationsRead(address, data.items[0].occurredAt)

Three properties the server owns so no client has to re-derive them:

  • Your own acts never appear. Collecting your own work records you as its creator AND its payee, so the filter has to work at the level of the act rather than the row.
  • One act is one item. A collect writes a mint plus its payment legs; they arrive collapsed, so subject and amountSbtc describe one moment.
  • amountSbtc is what you RECEIVED, not what the buyer paid. A 500-sat collect that splits 100 away pays the creator 400, and 400 is what the item carries. Both figures are real rows on the same transaction, which is precisely why rendering the wrong one looks correct.

Read state is server-side, so a phone and a laptop agree on what is new. Fetching the feed marks nothing read - markNotificationsRead is a separate call, made when a person actually opens the surface. Wiring it to the fetch would mark everything read the moment a badge is calculated, and nothing would visibly fail.

Scope, stated plainly: participant rows exist for collects and payments. Invites, access grants and transfers write none yet, so they cannot appear in a feed regardless of the filter.

sdk 0.14.103 + ui 0.7.106 (2026-08-21) - how much can actually go on this template

Two additions, both about the same question from opposite ends.

ReadabilityVerdict gains capacity - { comfortable, unclipped }, in characters, rounded to 50 and capped at the product's own limit. comfortable is what a composer would cap at; past it the solver has shrunk the type below the size the canvas can carry. unclipped is where text starts being lost, which is the harder floor and the one worth refusing at.

A too-wide column is deliberately not counted against either. It is a shape problem present at any length, and it reports through relaxed. Counting it gave a capacity of ZERO for twenty of twenty-five templates on first measurement - which reads as "you may write nothing" when the truth is "this box is the wrong shape at any length". So comfortable: 0 now means something specific and rarer: no amount of text on this canvas reads at the minimum size. Every landscape page template in the corpus is in that state, which is the same measurement that retired the landscape openletter canvas two releases ago.

pageStyles.getAuthoredCanvases(styleSetId) is new. It returns every authored canvas of a style set with the artifact's real width / height / aspect and its readability verdict, in one request rather than a preview call per canvas.

const { data } = await rtClient.pageStyles.getAuthoredCanvases(4)
data.canvases['openletter']  // AuthoredCanvas[]

AuthoredCanvas is the counterpart to SlotSpecCanvas: that one describes a slot an author MAY use and can state only an orientation, this one describes a canvas they HAVE authored and so can state facts about the file. Cross the two to tell an unauthored canvas from one the product type does not offer - they look identical on a picker, a canvas simply absent, and they mean opposite things. The first is work an artist can still do; the second is a slot the product type withdrew.

sdk 0.14.102 + ui 0.7.105 (2026-08-21) - a template can tell you it is hard to read

Validating a page style now returns a readability verdict beside the existing one:

const { data } = await rtClient.pageStyles.validateStyle(styleSetId)
data.data.validation.isValid   // unchanged: does the SVG satisfy the product spec
data.data.readability          // new: can this template set the worst letter the product admits

readability is ReadabilityVerdict | null and carries the setting the solver reaches at the product's own character limit - fontSize, lineHeight, measure (characters per line), effectivePx (apparent size at the width an app displays the canvas at) - plus relaxed, the bounds that had to be given up to get there, clipped, and a notes line per bound naming what to change. probe states the limit it measured against, so a verdict cannot be read as stricter than it is. Null means the product type enforces no character limit; a haiku cannot overflow a box drawn for a haiku.

previewDraft returns the same field, which is the more useful of the two: it answers the question while the SVG is still in the upload box.

It is advisory and will stay that way. Measured across the template corpus on 2026-08-21, five of twenty-five pass and twenty fail - so a gate here would refuse most of what the protocol itself ships. isValid does not move, errors does not gain a row, and the notes are deliberately kept out of warnings too, since landing them there would put them in front of every existing consumer as though they were spec problems.

What the twenty fail on is worth knowing, because it is not data loss: none of them clip. They fail on measure and effective size - every landscape template in the corpus carries a column between 89 and 164 characters against a readable ceiling of 75, which is the same measurement that retired the landscape openletter canvas one release ago.

StyleValidationResult also gains canvasId, which the route has always returned and the type never declared.

sdk 0.14.101 + ui 0.7.104 (2026-08-21) - a canvas names a slot, not a size

SlotSpecCanvas loses displayName, width, height and paperEquivalent. It is now { canvasId, orientation } and nothing else. This is a breaking change for anyone rendering those fields - templates.getSlotSpecs() is where they arrived.

The removed numbers were wrong for 74 of the 75 templates filed against them. letter-* slots advertised US Letter proportions while every file at that slot carried postcard ones; postcard-* slots advertised a postcard while the files were 2:1 ticket stubs. Nothing broke, because nothing rendered from them except a studio picker label and a call to getOrientation - and the orientation was correct for all 75 the whole time. So the claim that held is kept and the claim that never did is gone rather than corrected: renaming sixty files to make a slot's size true would have fixed nothing, and the drift returns the moment anyone authors the sixty-first.

getSlotSpecs is a create-time surface. No artifact exists when an author picks a slot, so nothing at that moment can know a shape without inventing one.

Style gains width: number | null and height: number | null - the artifact's own size, read off the SVG at ingest and stored on the row. That is where a real size now comes from, and it is a fact about the file rather than an expectation of the slot. Null means the row predates the columns or the SVG declares no usable size; a wrong number would be worse than no number.

GET /api/styles/:id/canvases returns { canvasId, orientation, width, height, aspect } per authored canvas, with aspect computed server-side so no client derives its own.

Migration, for the one label this affects:

- `${canvas.displayName} - ${canvas.width}x${canvas.height}`   // create-time: invented
+ `${canvas.canvasId} - ${canvas.orientation}`                  // create-time: what a slot declares
+ `${style.canvasId} - ${style.width}x${style.height}`          // after upload: the artifact's own

sdk 0.14.100 + ui 0.7.103 (2026-08-21) - the SDK stops choosing an orientation for you

pageStyles.validateStyle(styleSetId, format?) no longer defaults format to 'landscape'. Omit it and the product type's own defaultCanvas decides; the query parameter is simply not sent. Pass an orientation and behaviour is unchanged, so this is source-compatible for every caller that was already explicit.

The default mattered because openletter is now portrait-only. A landscape letter sets body copy in a column around 1000px wide - roughly 125 characters against a readable ceiling of 75 - and no type size fixes it, since shrinking the glyph fits more characters on the line. The product type stopped offering the canvas, so a caller who named nothing was being sent, by the SDK, at the one orientation the product does not have. That is a 400 rather than a render.

An unsupported orientation now answers 400 naming what the product does offer, where it previously surfaced as a 500. It is a request for something that does not exist, not a server fault.

ticketStyles keeps its landscape defaults, and correctly - a ticket genuinely is landscape.

Nothing else in the SDK surface moved. The larger change in this release is server-side: body copy is sized per letter by a solver rather than by a fixed number in each template's bindings, which takes the count of templates that silently clip a full-length letter from eight to zero. Clients need no change to benefit.

sdk 0.14.99 + ui 0.7.102 (2026-08-17) - an invite can be held by someone who did not mint it

The holder model. Until now the minter of an invite was the only party who could send it, so "an operator hands you an invite and you decide who to give it to" had nowhere to live. Invite gains holderUserId and assignedAt: who may SEND, orthogonal to the existing claimBinding / recipientUserId pair, which is who may CLAIM. Both can be set on one invite, and the claim path is untouched - the binding vocabulary it already validates against a Privy-verified sign-in is exactly what a holder sets when sending.

Assignment is deliberately not a status. An assigned invite stays DRAFT until someone sends it, so the state machine and every query over status keep their meaning. ProtocolEventType gains INVITE_ASSIGNED because that leaves the stream as the only record the handoff happened - and it carries previousHolderUserId, since re-assigning a still-DRAFT invite overwrites the column.

rtClient.invites.assign(id, holderUserId) is new (admin only; DRAFT only; re-assignable until sent), and invites.create accepts holderUserId so mint-and-assign is one atomic call rather than a create that can succeed followed by an assign that fails.

invites.send(id, binding?) takes an optional binding - { kind: 'EMAIL' | 'X_HANDLE', value } - applied atomically with the DRAFT -> SENT flip, so an invite can never be sent-but-unbound. A holder must supply one: they are addressing one person, and an unbound code handed to a holder would be a bearer key they could pass anywhere. An owner may omit it and keep whatever lock the invite was minted with - that call is unchanged in shape and behaviour. BEARER, STX_ADDRESS and USER stay with whoever mints.

invites.list({ scope: 'held' }) is new: invites assigned to the caller, which they may send. Always self-scoped - holding is personal, so there is no admin override and no cross-user holder read. The default owned scope is unchanged.

Invites now carry 25 credits by default. Omit fundingStxMicro and the protocol funds the claim with 25; an explicit value still wins, and an explicit 0 is honoured rather than re-defaulted.

Minting is Admin-only for the Alpha. The Early Adopter daily quota drops from 10 to 0, so getMintAuthority().canMint is false for every non-admin tier and the mint routes 403. Inviting has not been taken away from those tiers - it moved: they receive invites by assignment and send them. The finite-daily-cap and role-hierarchy mechanisms are unchanged and still tested; only the tier table that feeds them changed.

Self-dealing is blocked from both ends: a sender cannot bind an invite to their own verified email or X handle, and a holder cannot claim an invite they hold - the same block the owner has always had.

sdk 0.14.98 + ui 0.7.101 (2026-08-17) - sBTC balances stop reading 0

One constant, one live consumer, estate-wide symptom: SBTC_FT_ASSET_NAME said sBTC, and the shared useWalletBalance hook composes its Hiro fungible-token lookup key as &lt;contract-id&gt;::&lt;that constant&gt; - but the chain's key ends ::sbtc-token, the define-fungible-token name (the same on both networks; the official mainnet token and the testnet mock both declare it, and it equals the contract name). The lookup missed, so every sBTC balance in every app on the shared hook rendered 0 regardless of what the wallet held. The constant is now the real chain name.

Transactions were never affected, and this was verified rather than assumed: stacks.js's Pc...ft(assetIdentifier, tokenName) derives the wire asset name from the 3-segment identifier and drops the tokenName argument - a mined sBTC transfer's serialized post-condition already read sbtc-token while the constant said sBTC. The value change is wire-neutral on every builder and sign route.

sdk 0.14.97 + ui 0.7.100 (2026-08-16) - the tags trim rolled back

Correction to 0.14.96, caught by the tags CONTRACT suite on the ci-db dispatch: /api/tags DOES read search / page / limit (the typeahead contract - case-insensitive substring on name, clamped pagination), and the batch below wrongly deleted TagListParams and the params from listTags on a bad measurement (the route reads via an sp.get alias the reconcile's grep pattern missed). Restored: TagListParams { page, limit, search }, listTags(params?), the useTags(params?) hook, and server-side tag search at every call site that was temporarily filtering client-side. isActive / sortBy / sortOrder stay retired - those really were never read. Every other 0.14.96 trim was re-verified against its route's actual reads.

sdk 0.14.96 + ui 0.7.99 (2026-08-16) - the types stop lying

The batch roll behind the first live BYOW co-sign failure. A Leather wallet rejected an open-letter mint with IncorrectArgumentCount(4, 2) - the builder still sent [sentiment, note] after the mint base grew (fee, owner) - and the census that followed found a second, larger class: six sites still calling set-page-info / set-event-info, functions that no longer exist in any generated contract. This release fixes both classes and retires every typed-but-unread parameter the sweep found.

BREAKING - the product-info setter. set-page-info and set-event-info were renamed on-chain to set-product-info with a signature change: 8 params in the order (name, description, thumbnail, fee, fee-sbtc, currency-preference, transferable, offers-allowed); the old max-* values moved to the separate set-max-supply (slots) setter. Accordingly: buildSetProductInfoTransaction + buildSetMaxSupplyTransaction replace buildSetPageInfoTransaction / buildSetEventInfoTransaction; privy.signSetProductInfo + privy.signSetMaxSupply replace signSetPageInfo (the sign route moved to /api/privy/sign/set-product-info, and its updates body is TYPED now - the old Record&lt;string, string | number&gt; bag is how the drift stayed invisible). Gate the supply call on the value actually changing; it is its own transaction.

BREAKING - dead code is gone. buildListEventTransaction / buildListPageTransaction (no list-event / list-page exists on any contract), the whole challenges and achievements services and their admin hooks, the user XP trio, users.getUserActivityStats cleanup remnants, getCurrencyTypeById, getLocationById, updateLocation / deleteLocation, getFundingGroupStats, and resetCacheStats - every one called a route that has never existed. searchUsers now sends q (it sent search since April; the route rejected every call, so the method was dead the whole time).

BREAKING - honor-or-retire on every list params type. Fields the routes never read are GONE from the types: EventListParams loses startBefore / minPrice / maxPrice / sortBy / sortOrder (and gains endBefore, which the route always read); CollectionListParams renames appId to sourceAppId (the app-filter name convergence - the route honors both spellings) and loses sort/search; StyleSetListParams finally carries includeAppScoped (app-owned ticket sets were invisible to every SDK caller) and loses its dead fields; listEventTypes takes no params at all (see 0.14.97 above for the tags correction); locations / funding-groups / users / roles / link-types / currency-types / brackets types trim to what their routes read. Server-side, thirteen more list routes reject unknown query params with a 400 naming the allowed set - a misspelled filter can no longer silently return the unfiltered superset.

UserInstancesParams.sourceAppId is new, and it closes the highest-risk leak the app-scoping census ranked: /api/users/address/{addr}/instances is cross-app by design for the profile hub, so a client app rendering "your collection" without it shows editions minted in EVERY app - a bottle note as a signed letter. OTR's signed tab and grammata's My Collection both pass their own app id now.

CommandResult rejections carry a machine code (from RisingTideError), useCommand exposes it as errorCode, and useDeployProduct state gains the same field. Client error mappers match on the code FIRST and prose second - the access-wall finding fell through to a generic fallback precisely because the code was discarded in the hook.

AppSignupConfig.acceptedCurrencies is new on GET /api/apps/{id}/config: the app-level currency floor the sign routes already enforce, served so clients INHERIT it instead of hardcoding a fallback - an operator flip to sBTC-only now narrows the UI's offer without a redeploy. Campaign rules may narrow it further, never widen.

Also: IngestionHealth.undeliveredOccurrenceMs reaches the SDK mirror (the ingestion.silent breach source was invisible to rt-admin's card), the shared AuthDialog's icon is configurable via config.auth.appIconSrc (a client app must serve its OWN mark - the filename convention is not a brand), useWalletBalance derives the sBTC asset key from the SDK helper instead of declaring the principal a third time, and two new drift guards pin every builder AND every Privy sign route's argument count against the generated reference contracts - the class both live defects belonged to now turns red in CI instead of in a wallet.

sdk 0.14.95 + ui 0.7.98 (2026-08-15) - the operational mode becomes flippable without a redeploy

AppSignupConfig.mode is new on GET /api/apps/{id}/config: the operator-set operational mode (live, maintenance, coming-soon), or null for "no server opinion." The precedence rule is the whole feature: a set value wins over the app's compiled NEXT_PUBLIC_APP_MODE in both directions - flip an app into maintenance, or bring one out of a stale env-set maintenance, without touching Vercel; a null defers to the build, which is also why the column ships null everywhere (distributing this release changes nothing until an operator picks a mode); and an unreachable config keeps the client's last-known server value, then the env - so an API outage behaves exactly as it did before this release. The env var survives as the compile-time floor: it still covers pre-launch apps that predate their app row, and it is the lever that works when everything is down.

The client half lives in the shared AppModeProvider, which now accepts the SDK client and app id and polls the config once a minute - deliberately above ModeGate in the provider stack, because the gate unmounts everything below it during maintenance, and a mode read that dies behind the wall could never see the flag flip back. The poll rides useVisibleInterval, so hidden tabs and idle sleep pause it and an abandoned tab cannot hold a database branch awake. Apps that mount no gate are not polled - nothing would act on the answer.

Operators set the mode from rt-admin's app dialog ("Operational mode," beside the BYOW toggle): From build, Live, Maintenance, or Coming soon. Running clients pick a change up within a minute; maintenance walls everything, including /claim.

ProductInstance.archival is new: a server-derived object (state + optional categorical reason) carrying the six-state archival truth - live, archiving, archive_failed, archived, archived_propagating, locked. It is computed from the protocol's render and detach job tables plus the on-chain detached columns, so every client renders the same answer without knowing those tables exist. It rides the per-address collection listing, the single-instance read, and the archive-request response. Read it for permanence chips instead of inferring from detachedTokenUri - the two states that used to be invisible (a save in flight, and a terminal failure with no permanent copy) are exactly the ones it adds. Before this, a permanence-failed edition rendered identically to a healthy live one, forever, on a product whose pitch is permanence.

rtClient.productInstances.requestArchive(token, instanceId) is new - the owner re-save verb: "make or refresh my permanent copy now." One call serves Retry (after a failure) and Re-save (refreshing an archived copy); the server re-runs the whole render -> upload -> re-point arc, content-hash dedupe makes unchanged bytes free, and the response returns the freshly-derived archival state so a client renders server truth rather than flipping local state optimistically. The auto-detach pipeline's "already detached" condition was refined to "the chain pointer equals the CURRENT manifest", which is what lets a re-save's final leg re-point the chain at the refreshed copy.

There is no Lock in Alpha. The "Lock permanently" affordance is removed from the hub's collection surfaces per the owner re-save model - an upgradeable permanent copy, no freeze. lock-detached stays on chain (contracts are immutable) and the locked STATE still renders; no Alpha surface offers the action. signLockDetached remains in the SDK for the operator smoke and the future Lock-at-mainnet return.

AppSignupConfig.byowEnabled is new on GET /api/apps/{id}/config, beside requireInviteForSignup and in the same one-source-of-truth shape: whether this app's sign-in surfaces OFFER the bring-your-own-wallet path. It gates the offer, never auth validity - existing BYOW sessions and write paths keep working. The shared AuthenticationChoice honors it (composed with the existing NEXT_PUBLIC_ENABLE_BYOW compile-time kill), useProtocolUser().byowEnabled exposes it, and a ?byow=1 query shows the option for that load as the deliberate-testing escape - the invite wall, not the button, is the security boundary. Default true; operators toggle it per app from rt-admin without a redeploy. This is the Privy-only Alpha lever.

sdk 0.14.94 + ui 0.7.97 (2026-08-15) - the archival surface first ships

The storage Phase 1 distribution, to all ten apps, hours before 0.14.95: ProductInstance.archival, rtClient.productInstances.requestArchive, and the byowEnabled auth rider first shipped HERE. The 0.14.95 entry above describes that whole surface - both versions landed the same night and the later entry absorbed the write-up, which left this heading missing from the record. Server-side, the same distribution carried the guaranteed-fit archival pipeline: the quantize ladder with no unchecked third outcome, per-asset resume, sha256 dedupe so unchanged bytes are free, and the four archival identity tags.

sdk 0.14.93 + ui 0.7.96 (2026-08-14) - moderation gets a history

ProtocolEventType gains CONTENT_HIDDEN and CONTENT_RESTORED: an operator hid or restored user-published content.

The hide lever shipped in July with the row's hiddenAt / hiddenBy / hiddenReason triple as its only record. That triple holds current state, and un-hiding clears it - so a hide followed by a restore left a row byte-identical to one nobody had ever touched, and "was this taken down, by whom, and why" became unanswerable the moment it was put back. History cannot live in a field the next write erases.

Two names rather than one carrying a boolean, matching APP_ACCESS_GRANTED / APP_ACCESS_REVOKED - the same shape, a reversible operator action on a subject - and it keeps "what has been taken down" a filter on eventType rather than a payload inspection. They share one payload schema: targetType, an optional reason (on hide only - a restore has nothing to explain), and for an instance its parent productId, so "everything taken down on this drop" is one query. entityType is the moderated row itself, so a takedown history joins straight to its subject.

The emit is deliberately non-fatal: a takedown that succeeded must not report failure because its audit row could not be written. An operator hiding abusive content under time pressure should never be told to retry over bookkeeping.

Also in this release: GET /api/admin/smart-products, an operator browser that includes hidden rows by default. Until it existed, hiding a product removed it from the operator's own screen too - the read services had an includeHidden option, documented as exactly how an admin sees hidden rows to un-hide them, and nothing passed it. Recovery required SQL.

sdk 0.14.92 + ui 0.7.94 + conformance 0.5.1 (2026-08-14) - the testnet faucets become the protocol's

Both testnet faucets now belong to the protocol, are sized against what a user action actually costs, and leave a record.

rtClient.privy.signStxFaucet(token, { network }) is new, and what it replaces is the point. There was no protocol STX tap at all: every app POSTed api.testnet.hiro.so/extended/v1/faucets/stx directly from the browser for Hiro's fixed 500 STX - no auth, no per-user metering, no record it happened, and roughly 192 actions' worth of gas from a single click. The sBTC tap was ours but called a mock contract whose faucet() mints a Clarity define-constant 1 sBTC, which is 200,000 collects against a first create that earns a newcomer four. A faucet that out-pays earning by five orders of magnitude makes a create-to-earn economy decorative.

Both are protocol-wallet transfers from a float now, sized in user actions rather than round numbers, each with its own daily cap. signSbtcFaucet is unchanged at the call site - the amount moved server-side - so its docblock claiming it "mints 1 sBTC" was corrected rather than its signature.

useTestnetFaucet loses its stxAddress option. The server funds the authenticated caller, so a client can no longer name a recipient. An option that is silently ignored is worse than one that is missing: it invites a caller to pass a stranger's address. The hook also drops a fallback that opened Hiro's sandbox page in a tab and then reported status: 'success' - for a tab it had merely opened, while the balance poll found nothing.

FAUCET_GRANTED joins ProtocolEventType. A faucet tap was previously invisible to every projector, ledger and report, so "the loop turned" and "everyone tapped the faucet" were the same shape in the data. It carries recipient, currency, amount and app, and writes no Payment row - a grant has no counterparty earning and would pollute every earnings surface. Note currency records the chain asset (STX / SBTC) even where an app's own vocabulary differs: the stream records what moved on chain.

conformance 0.5.1 fixes a rule that had gone half-blind. sbtc-faucet-network-gate matched faucets/stx, sandbox/faucet and signSbtcFaucet - it named its subject by mechanism, so introducing signStxFaucet silently removed half its coverage while still reporting green. Adding the method exposed a second defect in the same rule: its network-gate pattern had no !== 'testnet', so the safest idiom - a fail-closed early return - would have been flagged as a violation while === 'mainnet' passed. Both fixed, both negative-controlled. No config change is needed; the rule id is unchanged.

sdk 0.14.91 + ui 0.7.93 (2026-08-13) - the SDK builds on machines that are not ours

No API surface changed. rt-sdk moves to moduleResolution: "bundler", fixing a build break only a clean install could see: the declaration-emit step type-checked a transitive dependency's TypeScript source (viem's nested ox, which ships .ts files) under the SDK's strict settings, and failed on any machine whose node_modules matched the lockfile. The development machine's tree predated the lockfile, so the failure was invisible there and appeared only in CI's first npm ci run - and would have appeared for any new consumer building from a fresh clone. skipLibCheck does not cover a dependency's .ts source (only .d.ts), and exclude does not stop the compiler following imports, which is why the fix is the resolution mode rather than a lint switch. Distributed with ui 0.7.93 across all ten apps in lockstep.

ui 0.7.92 + conformance 0.5.0 (2026-08-13) - the design system becomes an artifact

A protocol app used to be started by copying thirteen files out of a product app. rt-protocol/CLAUDE.md said so in as many words. That instruction is gone, and what replaced it is rt-sample - a real building app with a /gallery route rendering every component @rising-tide/ui/primitives ships, enforced by a test that derives the required list from the package's own exports.

New in @rising-tide/ui/primitives: Spinner, LoadingState, EmptyState, ErrorState, Toaster and ToastProvider. Button gains an xs size and Badge gains ghost and link - all three already existed in every client app, so the shared copy was behind its consumers.

Button's typeface is a token now. font-mono was hardcoded in its base string, which made it unadoptable by any brand whose buttons are not mono. It reads --rt-font-button with an inherit fallback. The money variant keeps mono unconditionally - that variant is sBTC settlement only, so its label is chain data.

Two things that were live defects rather than duplication. The Toaster in rt-events, rt-landing and rt-studio was a byte-identical copy of a version that predated two mobile fixes - a 44px dismiss target and safe-area offsets - which had landed in rt-profile and reached nobody. rt-events is installable, so it was shipping a dismiss button under the touch floor that could sit under a notch. And all four apps @sourced the shell bundle without sourcing primitives, which would have rendered the shared components unstyled.

Forking a primitive is now a supported, checkable outcome. checkButtonContract from @rising-tide/ui/testing reports which RULES a fork lost - 44px hit area per size, focus ring, non-interactive disabled - with no opinion about how it looks. Run against the three client apps: grammata passes cleanly, on-the-record misses two sizes, bracket-blitz has no floor at all.

extends: "rising-tide-design-system" opts a conformance config into checking its declared extension points. Absent it, nothing about the design system runs - installing this package for the sBTC facts never gets you told your buttons are wrong. See Design System.

Both packages now accept React 19 as a peer. @rising-tide/sdk and @rising-tide/ui declared peer react@"^18.0.0" while every app runs 19, so a bare npm install on a new app failed with ERESOLVE and an error naming React rather than us. Every existing app carried an undocumented .npmrc to work around it.

conformance 0.4.0 (2026-08-13) - a rule is deleted, and warn stops being a place to park

Breaking, in the direction that costs you nothing: install-namespaced-storage-key is gone. If you had it in allow, remove the entry - an unknown id is ignored, so nothing fails either way.

It was the last of two rules about the install-dismissal localStorage key, both written on a premise that was never true as a mechanism: that two apps could dismiss each other's install prompt. localStorage is scoped to an origin. Two apps on different domains cannot collide however they key, and subdomains of one registrable domain do not share it either - that is cookies.

The first rule went the day it shipped. This one was kept as a warning on a narrower argument - if you serve several apps from one origin at different paths, an un-namespaced key really is a collision, and we cannot see how you deploy. That argument is true, and it was still not enough. A warning whose precondition nobody meets is a line nobody can act on, and a report with lines like that in it stops being read closely. Three of four in-house apps carried it permanently; all four now report 0 warned.

So warn is now documented and enforced as one thing only: a notice period for a rule on its way to being an error. The package's own suite fails if a rule ships at warn without a version at which it becomes an error. A rule that cannot get there is deleted instead.

The lesson generalises past this package: controls prove a matcher matches; they cannot prove the hazard exists. Both deleted rules had positive and negative controls and clear messages, and neither had a defect behind it.

Two adjacent fixes rode along, both the same shape. The declares-*-surface failure messages tell you which rules stop running when a declaration goes stale; that sentence was hand-written and wrong in two of three surfaces, and is derived from the rule set now. And the message-contract suite had a hand-kept floor of "at least 11 rules produced a message", which is a second place to remember a number - it is derived too, with a negative control proving it can name a rule that has gone silent.

conformance 0.3.0 (2026-08-13) - the rules an app can check for itself

A new package, and the first one deliberately outside the SDK/UI tarball lockstep.

What happened. A handful of protocol facts fail silently - a hardcoded sBTC address renders a balance of zero, a mainnet faucet call reaches a function that does not exist, a missing viewportFit: "cover" makes every safe-area inset in an app compute to 0px with the markup still looking correct. They were enforced by a test suite inside the protocol API, which walked four client repositories by name, from a hardcoded list.

That could never cover an app the protocol had not heard of, which is the opposite of what a protocol is for. It also went green having checked a fraction of what it claimed the first time CI ran a single-repo checkout: two of the guards failed loudly on their vacuity floors, and three warned and passed - one reporting success over five of nine apps.

Now: npm i -D @rising-tide/conformance and npx rt-conform. Protocol rules run by your app against your app, plus four checks that keep the report honest, reading nothing outside your repository. Adoption is an 11-line conformance.config.json and a script line - no test runner required, and the CLI is a thin wrapper over a runConformance() you can call from a test if you have one.

Failure messages are the point. Most of these facts cannot be derived from the SDK's types, so every message states what is wrong, why the protocol cares, and the correct form. Measured on the first implementation: 164 of 195 lines were message text and 31 were matchers. A rule that only says what is wrong gets suppressed rather than fixed, so that contract is enforced by a test.

Every rule carries positive and negative controls, and they run before every real check. A matcher that quietly stopped matching would report a clean app and look identical to one. A failed control exits 2 - distinct from a conformance failure's 1 - because "this tool is broken" and "your app has a problem" otherwise produce the same silence.

Not a gate on access. It is not an access grant, it does not gate an API key, and it must never become one. You run it for your own benefit or not at all.

Three candidate rules were considered and rejected, and one of them shipped first and was corrected within the day - see Conformance for what does not ship and why.

ui 0.7.88 (2026-08-12) - apps stop polling when nobody is using them

A ui-only line, and the end of a thread that ran through 0.7.85 to 0.7.88 in one day.

What happened. One open browser tab held the testnet database awake for nine hours while every cron correctly reported itself idle. The cause was a 4-minute authenticated refresh in the shared auth provider, sitting inside the database's 5-minute autosuspend window, so a single backgrounded tab in any of the nine apps kept the whole thing running. A sweep for the same shape found seven more timers, including a health probe polling every 30 seconds from four apps - one of them a public page, so an anonymous visitor's open tab did it too.

0.7.86 and 0.7.87 stopped every network-reaching timer in the package from running while its tab is hidden, with the rule in one place (setVisibleInterval) rather than copied per call site.

0.7.88 replaces that gate with the signal it was approximating. Visibility turned out to be wrong in both directions: it reads hidden for a minimised window but visible for a window merely sitting unfocused behind another, and a two-second glance re-armed everything. Production logs showed the gated timer firing on 1 tick in 15 while minimised, plus three further requests from a focus handler no interval gate could reach.

So the trigger is now deliberate interaction. After ten minutes with no press, keystroke or scroll, one call to React Query's focusManager suspends every poll in the app at once: RQ intervals, the package's own timers, and the auth provider's refresh-on-return, which now subscribes to the same switch instead of listening to focus and visibilitychange directly. Focus deliberately does not count as activity, so glancing at a paused tab costs nothing.

Nothing is unmounted, no cache is cleared, nobody is signed out, and mutations never pause - a paused app is a fully working app whose numbers have stopped moving. Which is why it says so: the Testnet strip, already mounted in all nine apps, carries the paused state and becomes the resume control. Testnet only, derived from the new required network field on RisingTideProvidersConfig so mainnet cannot opt in by accident.

Also in this line: 0.7.85 collapsed five hand-rolled testnet faucets onto one useTestnetFaucet hook that owns the network gate, after four copies of a balance hook drifted and one started reading from a contract that returns 404.

SDK 0.14.89 (2026-08-11) - every broadcast now says whether it owes an event

The companion to 0.14.88. The ingestion floor gave the protocol a second route IN; this makes the protocol always know what is OWED.

The invariant: if a transaction will emit a chain event we handle, a PendingTransaction row must exist before it is broadcast. Without one a missed webhook is not merely unrecovered, it is undetectable - nothing knows the transaction happened, the reconciler has no work item, and no health signal can count what it cannot see.

An audit found 16 broadcast paths with no registration, 14 of them emitting handled events. signAndBroadcastTransaction is the single chokepoint for every server-signed transaction, so the registration is now a required argument there rather than a step each route remembers. An optional parameter is a thing the next caller forgets, exactly as the previous 16 did; a required one is a compile error. Exemptions are a distinct variant that must state their own reason, and only two qualify (the sBTC mock faucet, and a bare STX gas transfer).

The BYOW half closed too: useDeployProduct (shared by three apps, so BYOW deploy was asymmetric with Privy deploy in all of them), grammata's page-info edit, and bracket-blitz's tournament-admin actions all register now.

Also hardened: POST /api/pending-transactions had no auth guard at all - no entitlement, no API key, no bearer check - so anyone reachable could create arbitrary pending rows, feeding the reconciler work items for transactions that do not exist and holding a health signal in permanent breach. It now requires an authenticated caller, Privy or BYOW. ui 0.7.83.

SDK 0.14.88 (2026-08-11) - a second route in, and a signal that watches it

IngestionHealth gained chainFloorBehind: how many blocks the chain-sourced ingestion floor's furthest-behind cursor sits below the chain tip. null when there is nothing to measure, which grades ok - a failed read must not look like a breach.

The floor itself is the reason. Until now every protocol event reached the estate exactly one way: Hiro pushing a chainhook to the webhook. When that path is unavailable the events are not delayed, they are absent, and neither replay service can reach them - archive replay only replays payloads already received, and chain re-stream recovers by asking Hiro to re-deliver, so it routes through the outage. rt-api now also reads events straight from the Stacks API and routes them through the same handler pipeline, on a two-minute sweep, making the webhook the fast path rather than the only one.

Every other ingestion signal watches the chainhook path, so this one deliberately watches the fallback: if the floor stalls, the hook-shaped counts would keep reporting the outage the floor was covering for. Two ingestion alert keys that shipped in 0.14.87 (unregisteredHooks, productsWithoutPredicate) were also found to be unreachable by the alert emitter - they graded breach in the health document and paged nobody - and are now wired, with a derived guard so the two lists cannot drift again. ui 0.7.82.

SDK 0.14.87 (2026-08-10) - two health signals for a predicate that was never there

IngestionHealth gained unregisteredHooks and productsWithoutPredicate, both breaching at one or more. The existing unhealthyHooks counts chainhooks that registered and then broke, which leaves two blind spots: a hook stuck in PENDING has never registered at all (so it is neither FAILED nor ABANDONED, and has no uuid for the reconciler to evaluate against), and a product with no chainhook row whatsoever leaves every hook-shaped count at a healthy zero while its contract runs on chain with nobody listening.

The second signal counts a product whose contract is on chain either by being past DRAFT or by carrying a broadcast deploy txid. That second clause is the load-bearing one: the DRAFT to PUBLISHED transition is driven by the deploy event itself, so a status-only condition reads zero for exactly the failure the signal exists to catch - a product excludes itself by being broken in the watched-for way. Found by measuring against a live incident rather than in review.

Mirrors rt-api, and rt-admin's ingestion card now renders all three counts. Previously that card could go red with every visible number looking explainable, because none of the values that breach it were displayed. ui 0.7.81.

0.14.85 repointed the client-side sBTC addressing at the testnet mock's new home: the support contracts (sbtc-token, nft-trait) now deploy from the PROTOCOL wallet, restored automatically by the health cron after a chain wipe - so a wiped testnet heals with zero humans, and the BYOW post-condition builders and balance reads target the contract that actually exists.

0.14.86 retired managerContract end to end (a pre-collections vestige - "pages assigned to a manager contract" never grew an on-chain half; the value was only ever a string printed in deploy events). It left every request and entity type, the generate schemas, the templates, the deploy events, and the database. Also in 0.14.86: smartProducts.getPrivateNote(productId, instanceId) - the owner-authenticated read for the private-note capability (the public instance routes redact owner-only data, so this is the one door an organizer reads a purchaser's note through); the dead getUserActivityStats was deleted (its route never existed - the real per-user aggregate is getUserProfile().totals, which gained totalEditions and entitiesCollected); and the consumer-less useUserActivity hook left @rising-tide/ui. ui 0.7.79 - 0.7.80.

ui 0.7.78 (2026-08-08) - Privy 3.36 across the estate

Dependency-only. @rising-tide/ui moved to @privy-io/react-auth 3.36 together with all nine apps, including the @stripe/stripe-js peer that Privy's release requires but does not install. No surface change.

SDK 0.14.83 - 0.14.84 (2026-08-06/07) - one composition record, refusal over silence, one mint

0.14.83 made a product's composition ONE answer. A single per-domain policy resolves the capabilities, constraints and attributes a product is made of, and both the create path and contract generation call it - so the persisted productConfig, the shapeDescriptor and the deployed Clarity are three views of the same record (deploying also reconciles the record from what was actually generated). A composition that cannot work is now refused instead of silently degraded: composing a part into a slot the target template does not declare, or one that reads a base name the template does not define, is a 400 naming the problem - previously a haiku composing deploy-fee generated a valid contract that charged nothing and returned 200.

0.14.84 gave the mint the same treatment: one mint event (rt-edition-published) and one handler across pages, tickets and brackets, with per-edition data riding the generic rt-capability-recorded envelope. The ticket's holder name and the bracket's picks became first-class capabilities (ticket-holder, bracket-entry), and the old "Note to Organizer" became private-note - real, composable, and deliberately OFF-chain, because a note disclosed to one person must not be published permanently to everyone. The revenue class a mint books under now comes from the product's domain rather than a hardcoded type, which is what let a bracket entry stop booking as a writing edition. max-uses joined as a Constraint bounding how often an instance can be USED after mint (a door validation, a punch card), filling a use-guard slot only consume-style products declare. ui 0.7.77. See Capabilities, Constraints and Attributes.

SDK 0.14.82 (2026-08-06) - attributes, and one vocabulary across every product

The third composition kind. An Attribute describes what a product IS, alongside Capability (what it does) and Constraint (what it may not do). Unlike the other two it is per-product, touches no part of the mint, and is the only kind an owner can change after deploy. See Capabilities, Constraints and Attributes.

One vocabulary. Name, description, thumbnail and price were spelled three different ways on chain - page-name / event-name / tournament-name, min-publication-fee / min-reservation-fee / entry-fee - so an off-chain reader had to know a product's domain before it could read that product's own name. They are now the product-info attribute, and get-product-info returns the same eight field names on a page, an open letter and an event.

location, with Types. A physical venue used to be welded into every ticket contract as an eleven-field tuple, so an online event had to type a street address and a page could not have a location at all. location now accepts PhysicalAddress or PublicLink; both publish one get-location, discriminated by a kind field. A Type is frozen at deploy - only its value changes afterwards.

Breaking, and deliberately so. events.createEvent now takes location (the server's field names) instead of locationData, whose address / zipCode keys the route never read - every create failed on a required field. location.kind picks the Type and defaults to PhysicalAddress, so an existing caller that only sends an address keeps working. The contact block (phone, email) is gone: it was never part of a location and never held a value.

Location gains kind and url. On-chain, set-location is a new owner-gated setter, and burning a ticket now emits the shared rt-edition-closed rather than a ticket-specific name nothing listened for. ui 0.7.76.

SDK 0.14.81 (2026-08-05) - app-scoped style sets and per-product art

A style set can now belong to ONE app. The seed tags a set app-scoped + app:<slug>, and listStyleSets hides tagged sets unless the caller passes includeAppScoped - so a client-specific template (Bottle Drop's Fragment) stays out of every other app's style picker while remaining fully renderable by id. Alongside it, the per-product styleSetId now flows through the random-product read path, so a product renders in the style it was CREATED with rather than whatever the reading app has configured as its default. ui 0.7.75. See Style Sets.

ui 0.7.61 - 0.7.75 (2026-07-30 - 2026-08-05) - the shared-surface estate pass

A sustained pass over @rising-tide/ui itself, sdk-paired where noted above. The install estate: the two protocol PWAs label themselves "Hub" and "Events" on the rail (0.7.61), install icons moved to versioned paths so a redraw is actually reachable past immutable caches (0.7.64), and the app rail shows each app's own glyph rather than one shared mark (0.7.66). The auth surface: the shared AuthDialog fits a phone, scrolls reliably, starts below the testnet strip via --rt-overlay-top-inset (0.7.64), and puts the shared auth errors on the right step (0.7.68). Correctness: 36 colour sites that had been compiling to dead CSS under Tailwind v4's parenthesis shorthand render again (0.7.63), PriceDisplay applies the sub-0.001-reads-in-sats rule with optional USD (0.7.71), and useUserByAddress became a real React Query hook (0.7.72). Hygiene: five consumer-less hooks were deleted outright (0.7.73 - useTransactionStatus deliberately kept as the close-on-broadcast exception's documented escape hatch), a @rising-tide/ui/testing entry point ships the provider harness tests need (0.7.70), and the safe-area utilities moved into a shipped safe-area.css so @utility survives the node_modules import (0.7.74).

SDK 0.14.79 - 0.14.80 (2026-07-29/30) - honest profile cards; the design floor

0.14.79 fixed the profile OG cards in every app that renders one: they fetched an endpoint that has never existed, so every card advertised its creator as having done nothing. They now read the profile aggregate's totals block, which gained uniqueCollectors and uniqueCreatorsSupported in the same change. 0.14.80 carried design-system adherence phases 0-4 across the protocol apps: the two-word wordmark plus an app crumb in both shells, a footbar that reads real chain heights and package versions, and the money button's label lifted from 3.01:1 to 6.4:1 contrast. ui 0.7.59 - 0.7.60.

SDK 0.14.74 - 0.14.78 (2026-07-29) - explicit generate modes; the SDK becomes the contract

Two deliberate breaks in one day. First, contract generation now requires an explicit mode: preview renders and pins nothing, deploy is the real thing - the ambient default was doing deploy-shaped work on preview-shaped calls. The dead generate surface that ambiguity had accumulated was deleted outright (0.14.76), and a ticket token-URI fix landed the same day (0.14.77). Second, @rising-tide/ui finished routing every rt-api call through typed SDK methods (0.14.78, ui 0.7.56 - 0.7.58; ui 0.7.53 - 0.7.55 rode the same day's SDK bumps): the app rail and access gate were the last raw fetches, and closing them is what makes "the SDK is the contract" a checkable property rather than a convention - a route the SDK cannot reach is now a route the shared UI cannot silently depend on.

SDK 0.14.68 - 0.14.73 (2026-07-28) - the EVENT domain grows up

The rt-events calendar work gave the EVENT product type a real shape end to end: the dead reservation read path was repaired, the ticket wallet reads a user's reservations cross-app, and validate-reservation runs on chain - a door check against the contract rather than a database row. The formatSbtcDisplay helper also shipped in this span (0.14.73): any sBTC amount under 0.001 reads in sats, the display rule every app has since adopted. ui 0.7.45 - 0.7.52.

SDK 0.14.64 - 0.14.67 (2026-07-27/28) - one revenue path, and the mirrors that stay true

0.14.64 finished the canonical-event-stream revenue work: every payment lands through one path and stream defects are visible instead of swallowed. 0.14.65 exists because 0.14.64's tarball shipped a BOM'd package.json (a Windows PowerShell encoding trap; npm pins the bad bytes by integrity hash, so recovery is always a fresh version). 0.14.66 - 0.14.67 closed all five SDK/Prisma enum-mirror drifts and added the guard that keeps them closed - the SDK's enums are now asserted against the schema's, so a renamed database enum fails a test instead of a runtime. On the ui side, 0.7.40 shipped AppShell - the mobile-first shell (safe-area-aware, bottom tab bar) that rt-profile's hub runs on, beside the frozen desktop ShellLayout. ui 0.7.40 - 0.7.44.

SDK 0.14.62 - 0.14.63 (2026-07-23) - invites locked to a verified identity

An invite can now be locked to a specific verified identity, not just handed to whoever opens the link first. InviteClaimBinding (BEARER | EMAIL | X_HANDLE | STX_ADDRESS | USER) plus bindingValue on the Invite read model and the create/update inputs name the lock, and the claim is checked against a Privy-verified token attribute (email, X handle) or the authenticated wallet - never a self-reported profile field. 0.14.62 shipped the bindings and surfaced boundTo (the lock type only, never the target value) on the invite preview, so a claim screen can prompt the right sign-in. 0.14.63 added rtClient.invites.precheckClaim(code) (returns InvitePrecheck), a read-only eligibility check so a client can catch a mismatch right after sign-in - before asking a new user to set up a profile - instead of only when the atomic signup rolls back. ui 0.7.39. See Invites.

SDK 0.14.59 - 0.14.61 (2026-07-22) - per-app access grants

Added a third authorization axis alongside entitlements (what a user may do) and feature flags (what an app has turned on): per-app access (where a user may act). Every (user, app) pair is a first-class AppAccessGrant - minted on invite claim or by an operator, revocable, enforced fail-closed on the acquisition and participation routes. 0.14.59 made access an enforced grant and retired the legacy addUserApp / removeUserApp. 0.14.60 added the operator surface rtAdmin.appAccess (grant / revoke / roster) plus App.accessPolicy (PUBLIC | GRANTED) and App.catalogTease. 0.14.61 turned the cross-app rail into a catalog view - each entry carries a RailEntryState (OPEN | UNLOCKED | LOCKED) - and added rtClient.apps.getAccess (AppAccessCheck); the @rising-tide/ui AccessGate then gates a locked app with a teaser rather than a bare 403. ui 0.7.32. See Entitlements.

SDK 0.14.58 (2026-07-21) - invite mint authority

Added rtClient.invites.getMintAuthority() so a client can gate its invite-mint UI up front on the caller's tier-derived authority (mint permission is derived from the owner's role tier, not a dedicated entitlement) rather than showing a form that only 403s on submit. ui 0.7.30. See Invites.

SDK 0.14.57 (2026-07-21) - honest rate-limit back-off

The SDK now honors rate-limit back-off end to end. When a route replies 429, RisingTideError surfaces as a RateLimitError carrying the parsed Retry-After and RateLimit-* headers, so an app backs off precisely instead of guessing; server-side, the shared header block is applied uniformly across the throttled routes. ui 0.7.29.

SDK 0.14.55 - 0.14.56 (2026-07-06) - invite gating simplified; type-mirror sync

0.14.55 collapsed invite gating out of the client AppMode: whether signup requires an invite is now one server flag (App.requireInviteForSignup, read via apps.getConfig and surfaced as useProtocolUser().requiresInvite), so AppMode is operational-only (live | maintenance | coming-soon). 0.14.56 synced deferred type mirrors (ProtocolEventType, IngestionHealth). ui 0.7.27.

SDK 0.14.54 (2026-07-04) - create-to-earn: STX-payable create

Gave STX and sBTC distinct jobs in onboarding. The deploy path now threads a settlement CurrencyCode through both the custodial and BYOW routes, so a client can charge its create/deploy fee in STX while collects settle in sBTC. This is the engine behind the create-to-earn economy - gifted STX pays to create (and covers gas), earned sBTC pays to collect - so a funded newcomer's starter credits cover making their first product, not just fees. sBTC stays the primary settlement currency for collects; nothing changes for existing collect flows. 36 new SDK tests. See The Create-to-Earn Economy.

SDK 0.14.53 (2026-07-04) - multi-app invite preview

InvitePreview gains grantApps, the ordered set of apps a funded invite will grant (rt-api resolves the invite's grantAppIds to app names in order), so a claim landing can show the full set of apps a newcomer is about to receive before they claim. Additive - the post-claim path already provisioned the grants in 0.14.52. ui 0.7.25. See Invites.

SDK 0.14.52 (2026-07-03) - funded invites

Invites can now arrive carrying value. CreateInviteInput and the Invite read model gain grantAppIds (a set of app grants, number[]) and fundingStxMicro (starter STX in microSTX), and claiming an invite provisions the UserApp grants and funds the new wallet with STX once the account commits. This is the onboarding path behind funded invites: a newcomer lands already in the right apps with starter credits to act immediately. It pairs with a server-side ProtocolSigner plus an STX gas float (with a Hiro testnet fallback that funds even before a protocol wallet exists). ui 0.7.24. See Invites.

SDK 0.14.51 (2026-07-01) - letter identity read model

Denormalized author and minter identity onto the shared read model so every openletter app credits the right person after a transfer. ProductInstance gains minterAddress and minterDisplayName - the edition's original minter, distinct from its current owner - and RandomProductSummary gains authorDisplayName; the instances endpoint returns them automatically. Because editions are NFTs, writer-vs-holder is a real split the moment one moves: On The Record now credits the signer of a co-signature rather than whoever holds it now, while transfer and burn stay gated on the current holder. ui 0.7.22.

SDK 0.14.50 (2026-07-01) - edition provenance (minter)

Made the minter a first-class, persisted identity rather than something inferred from the mint transaction at read time. ProductInstance.minterAddress is frozen at mint and never touched by transfer, and random-service returns the note writer (minterAddress plus minterDisplayName) distinct from the current holder; RandomFeedbackEdition gains those fields. Rescoped mid-flight from a Bottle Drop note-authorship fix to a protocol-wide edition-provenance capability, since a transfer is chain-native and applies to every edition the moment it moves. ui 0.7.21.

SDK 0.14.49 (2026-07-01) - author name and letter salutation

random-service resolves PageDetails.author to authorDisplayName, so a rendered letter's "From" shows a name rather than a raw address. The salutation was centralized: the static "Dear " text was removed from all twelve openletter templates, and formatRecipientSalutation now returns the whole salutation - Dear {name}, for a named recipient, To whom it may concern, for an empty one - which is what lets Bottle Drop address a letter to whoever finds it. ui 0.7.20.

SDK 0.14.48 (2026-06-30) - capabilities on the bracket (PREDICTION) inputs

Completed explicit composition across all three domains. BracketContractProcessRequest now carries capabilities?: CapabilityConfigEntry[] and constraints?: ConstraintConfigEntry[], so a client composes a prediction (bracket) product from capabilities the same way it does a page or a ticket. The bracket contract was conformed to the canonical mint ABI (a canonical edition-data map + the mint slots), so the same capability - e.g. feedback - composes onto a bracket unchanged. Behavior only; all economics stay server-derived. See rtClient.smartProducts.

SDK 0.14.47 (2026-06-30) - capabilities on the EVENT inputs

Extended explicit composition to the EVENT domain. EventContractProcessRequest and CreateEventInput now carry capabilities?: CapabilityConfigEntry[] and constraints?: ConstraintConfigEntry[], so a client composes a ticket-domain product from capabilities the same way it does a page. The protocol persists them into the EVENT productConfig and shapeDescriptor, and the matching capabilities on the generate request fill the canonical mint slots (the ticket contract now exposes the canonical edition-data / edition-owner / next-edition-id names). Behavior only; all economics stay server-derived. See rtClient.smartProducts.

SDK 0.14.46 (2026-06-30) - self-serve product composition

Surfaced explicit composition on the create and generate inputs. CreatePageInput and ContractProcessRequest now carry capabilities?: CapabilityConfigEntry[] and constraints?: ConstraintConfigEntry[] alongside the existing recipe - so a client composes a product directly from capabilities and constraints, the self-serve alternative to a named server-owned recipe (the two stay mutually exclusive). Behavior is client-expressible; all economics stay server-derived and enforced - a deploy fee names only its feeSats amount, never its recipient (resolved to the app payout wallet, else the protocol fee address). See rtClient.smartProducts.

SDK 0.14.31 - 0.14.32 (2026-06-19) - detachable capability re-gated to the edition owner

Moved detach authority from the page author to the current owner of each edition, and made permanence per-edition. 0.14.31 retired the bulk surface shipped two days earlier - buildSetDetachedUrisTransaction, signDetachAllEditions, and the DetachAllEditionsParams / DetachAllEditionsResponse types were removed - and made SetDetachedUriParams.uri optional: when omitted the server pins the edition's existing permanent-storage manifest, so an owner detaches in one call. 0.14.32 turned the one-way lock per-edition - LockDetachedParams, LockDetachedTransactionParams, and buildLockDetachedTransaction now take an editionId - and added ProductInstance.detachedLocked (true once the owner has frozen that edition's storage choice for good). Authority is the current edition owner throughout, with a set-only carve-out so an author can pin edition #1 (the auto-minted gift edition) for its recipient but can never lock it.

SDK 0.14.30 (2026-06-18) - substrate health surface

Added the read-only substrate health surface for operators: AdminHealthService on RisingTideAdmin (rtAdmin.health.get()), backed by GET /api/admin/health (master-key gated). It returns a SubstrateHealth document graded against SLO thresholds - ingestion block and time lag, per-projector staleness and backlog, render-queue depth, dead-letter-queue counts, and pending-transaction age - with the HealthStatus (ok | warn | breach), IngestionHealth, ProjectorHealth, RenderQueueHealth, DlqHealth, and PendingTxHealth types mirrored from rt-api. Backs the rt-admin /system/health panel. No core client surface change.

SDK 0.14.25 - 0.14.29 (2026-06-17) - detachable capability invocation surface

Made the detachable capability invocable. Detaching pins an edition's on-chain token URI to a permanent-storage URL so the product resolves its metadata without the protocol API. 0.14.25 added the single-edition primitives buildSetDetachedUriTransaction (BYOW, SetDetachedUriTransactionParams) and the signSetDetachedUri Privy method (SetDetachedUriParams). 0.14.26 added server-driven orchestration for a whole product, signDetachAllEditions (DetachAllEditionsParams / DetachAllEditionsResponse), bounded per call and resumable. 0.14.27 added the batch builder buildSetDetachedUrisTransaction (SetDetachedUrisTransactionParams, up to 50 editions per transaction). 0.14.28 exposed ProductInstance.detachedTokenUri, non-null once an edition is detached. 0.14.29 added the irreversible permanence lock buildLockDetachedTransaction / signLockDetached (LockDetachedTransactionParams / LockDetachedParams). Authority at this stage was the contract owner; 0.14.31 re-gates it to the edition owner and removes the bulk surface. See Storage.

SDK 0.14.22 - 0.14.24 (2026-06-15) - rt-studio multi-canvas edit and brand sets

0.14.22 added per-canvas edit primitives to StylesService - addStyle, updateStyle, and deleteStyle operate on a single canvas (Style) within a set (the server re-derives and validates bindings; any edit reverts a live set to DRAFT for re-review; deleteStyle refuses the last canvas). 0.14.23 gave addStyle an explicit productType so a canvas attaches to the right type on a cross-domain theme set (one styled SVG spanning basic-page / openletter / haiku / tanka / sonnet). 0.14.24 landed brand sets: setProductTypeRoyalty(styleSetId, productType, royaltyPercentage | null) writes a per-piece royalty override, StyleSet.tags plus the sticker styleSetId association group an artist's line for discovery (stickers stay global in use), and StyleSetProductType.artistUserId is the per-piece author hinge. Zod mirrors updated. See Style Sets.

SDK 0.14.21 (2026-06-14) - per-asset earnings

Added settled per-asset earnings: StylesService.getEarnings(styleSetId, timeframe?) and StickerService.getEarnings(id, timeframe?) return an AssetEarnings document - per-currency totals, a per-product breakdown, and a recent payment tail - scoped by an AssetEarningsTimeframe (7d | 30d | 90d | all). This is the earnings counterpart to getContributions, which reports usage only; it is backed by a per-payment asset-attribution field so each settled cut records which template or sticker earned it.

SDK 0.14.19 - 0.14.20 (2026-06-14) - rt-studio approval workflow and template authoring

0.14.19 added the asset approval lifecycle: the SubmissionStatus type (DRAFT | SUBMITTED | APPROVED | REJECTED), a submissionStatus field plus review-audit fields on StyleSet and Sticker (with the StyleSetSchema mirror), submissionStatus / includeUnapproved list filters, and the workflow methods submitStyleSet / reviewStyleSet and submitSticker / reviewSticker (artist submits, operator approves or rejects with feedback). Catalog pickers default to APPROVED only. 0.14.20 added the template-authoring path: TemplatesService.getSlotSpecs() returns each product type's required and optional data slots and compatible canvases (PageSlotSpec / SlotSpecCanvas / PageSlotSpecResponse), StylesService.previewDraft() does a dry-run derive-validate-render with no persist (DraftPreviewResult), and createStyleSet's per-style dataBindings became optional so the server derives them from the SVG's data-slot markers.

SDK 0.14.17 - 0.14.18 (2026-06-14) - rt-studio writable asset surface and per-asset usage

Brought the creator asset surface online for rt-studio. 0.14.17 made StickerService writable, adding createSticker and updateSticker (the service was read-only). 0.14.18 added per-asset usage reads: StylesService.getContributions(styleSetId) and StickerService.getContributions(id) each return the Contribution[] rows an asset generated (one per product that used it, TEMPLATE_ARTIST or STICKER_ARTIST), so a studio can show where a template or sticker is in use. createStyleSet also gained artistUserId, royaltyPercentage, and productTypeSlug, plus a per-style canvasId (canvas-registry id, which wins over format).

SDK 0.14.16 (2026-06-13) - rt-admin observability surface

Added the dynamic cron registry read crons.list() (+ CronJobDescriptor; backed by GET /api/admin/crons, derived live from vercel.json so a newly-scheduled cron appears without editing a client list) and crons.runPath() for triggering any registered cron by its path. Added typed operator chainhook reads chainhooks.listAdmin() and getAdminHealth() with AdminChainhookRecord / AdminChainhookListData / AdminChainhookHealthData. These back the rt-admin /system/cron-jobs and /system/chainhooks pages, which previously used a hardcoded cron list and raw HTTP reads.

SDK 0.14.14 - 0.14.15 (2026-06-12) - contract-template retirement

0.14.14 added templates.getContractComposition() (+ ContractComposition / ContractParameterSpec), exposing the live on-disk fragment composition per product type - the ordered fragment manifest, post-processors, parameters, and domain capabilities - for the rt-admin Contracts surface. TemplateProcessRequest / TemplateProcessResponse were reshaped to key on productType (the harness composes live) instead of a DB templateId. 0.14.15 removed the retired DB-backed template surface: listTemplates, getTemplateById, createTemplate, updateTemplate, deleteTemplate, and the ContractTemplate / ContractTemplateParameter / TemplateListParams types (the dead @rising-tide/ui useTemplates hook - their last consumer - was removed in ui 0.7.4). The ContractTemplate DB table was dropped; the on-disk fragment system is the sole template authority and rt-admin reads composition live.

SDK 0.14.12 - 0.14.13 (2026-06-11) - currency policy and preference

Adds the app/collection accepted-currency policy and the user preferred-currency default. App.acceptedCurrencies (CurrencyCode[], sBTC-required primary floor) plus an optional per-collection rules.acceptedCurrencies override declare which currencies a product settles in; User.preferredCurrency migrated from a legacy Int to the named CurrencyCode enum ('STX' | 'SBTC' | null). New SDK helpers resolvePaymentCurrency (sBTC-biased: preferred-if-accepted, else sBTC, else first), isCurrencyAccepted, currencyFromPreference, and CURRENCY_DESCRIPTORS - formatCurrency now reads per-currency decimals from the descriptor instead of branching on 6/8. The four Privy sign routes enforce the policy server-side: a currency outside the accepted set is rejected (400 CURRENCY_NOT_ACCEPTED), and an omitted currency resolves to the sBTC primary (was a blind STX default). 0.14.13 adds AppsService.updateApp. BYOW transaction builders are now network-aware for sBTC (testnet mock vs official mainnet contract). See Payment Currency Policy.

SDK 0.14.11 (2026-06-10) - retire dead stats and leaderboards surface

Removed StatsService.getStats and getRevenueStats, the entire LeaderboardsService, and PaymentsService.getTopCustomers and getRevenueReport. Their /api/stats, /api/stats/revenue, /api/leaderboards, and /api/payments/reports/* routes were dropped in the protocol-stats-consolidation work and never replaced, so the methods only ever returned 404s. The orphaned response types went with them (RevenueStats, CurrencyVolume, LeaderboardEntry, LeaderboardsResponse, TopCustomer, RevenueReport). For protocol-wide totals use analytics.getProtocolStats() (GET /api/protocol-stats); the live payment reads payments.listPayments, getUserRevenue, and getProductPayments are unchanged.

SDK 0.14.10 (2026-06-10) - remove orphaned Contract types

Dropped the dead Contract and ContractListParams interfaces and the ContractTemplate.recentUsage field, left behind when the Contract table was retired (SmartProduct now carries contract identity). The companion @rising-tide/ui 0.7.1 removed the unused useContracts hook. The contract-template generation surface (processWritingContract, processEventContract, processBracketContract) is unaffected.

SDK 0.14.9 (2026-06-10) - SmartProduct catalog metadata

SmartProduct gains a catalog name (required) and description, the persisted mirror of the on-chain page-name / page-description. These are distinct from the visual title / subtitle that feed the SVG template slots: catalog metadata powers search and token metadata, while title/subtitle drive the rendered image. On creation, name/description default to title/subtitle (a single creator input), so existing clients need no change.

SDK 0.14.8 (2026-06-10) - contract identity denormalized

Following the Contract-table retirement, Transaction.contractId (a numeric FK) became contractIdentifier (a deployer.name string) and Chainhook.contractId became contractName plus contractDeployer. ContractsService.listContracts and getContractById were removed (the /api/contracts list and detail routes are gone); the process* contract-generation methods remain.

SDK 0.14.5 - 0.14.7 (2026-06-10) - rt-admin protocol resync

Added operator read surface backing the rt-admin refresh: per-product ProductStats, the capability/constraint registry, and role-entitlements endpoints with their SDK methods, plus a TemplateProcessResponse shape fix. 0.14.7 added the role-entitlements editor surface (admin entitlement CRUD) powering the RoleDialog.

SDK 0.14.4 (2026-06-06) - cron operator surface

Added AdminCronsService on RisingTideAdmin (rtAdmin.crons), powering the rt-admin Cron Jobs panel that lists the registered cron routes and their schedules. No change to the core client surface.

SDK 0.14.3 (2026-06-05) - sentiment-letter recipes

Added the recipe field to the contract-generate and smart-product-create requests. A named server-side recipe expands into the capability / constraint / contribution bundle, so a client passes one recipe to both calls instead of assembling the policy surface by hand. Backs the open-letter sentiment product.

SDK 0.14.2 (2026-06-04) - sentiment snapshot

Added the SentimentSnapshot type, its schema, and the getSentimentSnapshot accessor: the off-chain aggregate (co-sign counts, conviction) read back on a sentiment-enabled product.

SDK 0.14.1 (2026-06-03) - composable capabilities

Added the capability / constraint registry and config schema for productConfig.capabilities[] / constraints[]. A product type composes optional on-chain behaviors (mint windows, sentiment, and so on) by enabling registry entries at creation, validated against the registry at the write boundary.

SDK 0.14.0 (2026-06-03) - headless SDK plus @rising-tide/ui

BREAKING: the SDK is now headless. All React, auth, Privy, shell, design-token, and provider surface moved to the new @rising-tide/ui package (/react, /auth, /privy, /shell, /tokens, /providers). The SDK keeps the core client, admin client, and schemas (@rising-tide/sdk, /admin, /schemas). Apps import the provider stack from @rising-tide/ui/providers and consume both tarballs in lockstep. The deprecated rt-design package is gone.

SDK 0.13.24 (2026-06-02) - transaction polling fix

useTransactionStatus gained startPolling(txIdOverride?) with abort-supersede and a cancellable pollTransactionStatus, fixing a hang where useBurnEdition and useTransferEdition polled a stale transaction id and never resolved. Mint and signature flows were unaffected.

SDK 0.13.23 (2026-06-02) - actor taxonomy rename

The ActorKind values SYSTEM and CHAINHOOK were renamed to PROTOCOL (crons, backend, auth failures) and CHAIN (on-chain-caused, chain-technology-independent); USER and ADMIN are unchanged. The actor is now threaded per caller through the ProtocolEvent stream.

SDK 0.13.22 (2026-06-02) - ProtocolEvent stream expansion

Broadened ProtocolEvent coverage: enriched INSTANCE_BURNED, five product-lifecycle emits, USER_CONNECTED, PROFILE_UPDATED, AUTH_FAILED, and PAGE_UPDATED, plus from / to addresses on PAYMENT_RECORDED. Added AdminRenderJobsService for the rt-admin render-jobs operator card.

SDK 0.13.21 (2026-06-01) - reorg rollback: burn and deploy orphans

New ProtocolEventType value INSTANCE_BURN_REVERTED. Emitted by the confirmation-depth orphan sweep when a never-re-mined close (burn) transaction is walked back: the instance is un-burned, its currentInstances slot is restored, and the analytics projection that tracks burns is decremented. The same sweep also reverses a never-re-mined deploy by flipping its SmartProduct back to DRAFT (no event needed - the deploy handler sets status directly). See Chainhooks for the reorg model.

SDK 0.13.20 (2026-06-01) - reorg rollback: mint orphans

New ProtocolEventType value INSTANCE_ROLLED_BACK. Emitted when a never-re-mined mint (edition or reservation) is reversed: the orphan instance, its payments, and its transaction are deleted and the cumulative analytics projections (product, protocol, app, creator, collection) are decremented from the self-contained event payload. This is the rare-case backstop; the common reorg case (the same transaction re-mining moments later) is handled non-destructively by the webhook reverse path with no orphan left behind.

SDK 0.13.19 (2026-06-01) - pending-transaction lifecycle simplified

PendingTxStatus collapses to BROADCAST -> CONFIRMED on the happy path (plus REPLAYING, FAILED, DROPPED, HANDLER_FAILED). The transient MINED and RESOLVED states were removed - "mined" is now a column fact (block height and timestamp), not a status. Clients polling pendingTransactions.getStatus(txId) key terminal success off CONFIRMED.

SDK 0.13.18 (2026-05-31) - pending-transaction admin surface

Adds the operator-facing AdminPendingTransactionsService (list, reconcile) on RisingTideAdmin, powering the rt-admin pending-transaction monitoring card. No change to the core client surface.

SDK 0.13.15 (2026-05-31) - pending-transaction reconciliation

New pendingTransactions client surface: rtClient.pendingTransactions.register(input) and .getStatus(txId), plus the PendingTransaction / PendingTxStatus types. Substrate records are born confirmed from the chainhook, so a missed or delayed chainhook would otherwise leave no server-side trace of an expected confirmation. Registering a broadcast transaction id lets a reconciler cron re-read the chain and replay a stuck confirmation through the existing idempotent evaluate path. The Privy publish/collect paths self-register server-side and only need to poll getStatus; bring-your-own-wallet flows call register at broadcast time.

SDK 0.13.14 (2026-05-30) - substrate trifecta closeout

BracketContractProcessRequest.smartProductId?: number added. The BracketContractProcessRequest interface was declared flat rather than extending the shared ContractProcessRequest base, so the override field needed an explicit add (with JSDoc matching the base). With this in place, all four client apps (grammata, on-the-record, bracket-blitz, rt-admin) now use the substrate-derived creators[] path via useDeployProduct.generateContractCode(smartProductId). The bracket-blitz useCreateTournament hook was the final consumer to adopt the canonical flow.

Server-side companion: contributionService.targetSumForProduct returns 100 for PAGE and EVENT products and (100 - poolPercentage) for PREDICTION products. Bracket ProductContribution rows now correctly sum to the remainder after pool allocation.

SDK 0.13.13 (2026-05-29) - ConditionalProviders structural refactor

Fixes a latent race across all 7 client signin flows where the SDK's ConditionalProviders tree shape changed shape based on authMethod. The remount cascade could lose Privy click-handler context. Tree shape is now stable - PrivyTokenProvider always mounts (was previously documented as conditional). Apps that consume AuthDialog -> AuthenticationChoice (OTR, bracket-blitz) inherit the fix transparently; grammata's SignInDialog click-handler privyLogin() call is kept for popup-blocker UX but the comment block was rewritten to reflect the new stable-tree reality.

SDK 0.13.11 - 0.13.12 (2026-05-28) - chainhook + protocol-event tightening

0.13.11: enableChainhook now requires ?network= query param. The previous signature silently defaulted to testnet; the missing param surfaced as orphaned mainnet chainhooks. rt-admin updated to pass the network.

0.13.12: ListProtocolEventsParams gains optional productId. The rt-api productId filter expands the query to parent SmartProduct events AND child ProductInstance events in one call, fixing the rt-admin Blockchain Monitoring per-product card that previously missed INSTANCE_* events.

SDK 0.13.10 (2026-05-28) - chainhook re-register type tightening

Tightened the enableChainhook parameter type so the unused eventId field is now a compile-time error rather than a silently-ignored runtime field. Caught a stale eventId pass-through in rt-admin's useCreateEvent chainhook re-register that had been masking the (correct) server-side contractName + contractDeployer resolution path.

SDK 0.13.9 (2026-05-27) - generateContractCode callback

useDeployProduct.DeployFlowCallbacks gains an optional generateContractCode(productId): Promise<string>. When set, the hook calls it after createDraft resolves with the SmartProduct id, so the consumer can pass smartProductId into contracts.process*Contract() and let the API rewrite creators[] from the frozen ProductContribution rows. This is the substrate-derived create path used by grammata, on-the-record, and (as of 0.13.14) bracket-blitz.

Substrate Phase 5 smoke (fresh PAGE deploy on testnet) validated the path end-to-end and closed nine cutover regressions in the same session.

SDK 0.13.3 (2026-05-25) - CreatorsSupported aggregate

New GET /api/users/address/{stxAddress}/supporting endpoint plus matching SDK method users.getUserSupporting(stxAddress, params?) and React hook useUserSupporting. Inverse of getUserCollectors: returns the creators a user is supporting (one row per distinct parent SmartProduct.ownerAddress of editions the user owns), with edition and product counts plus first/last support timestamps. Powers the rt-profile /earnings Support tab "Creators You Support" panel.

The 4 rt-* consumers had been pinned at 0.13.1 (skipped the 0.13.2 hop that grammata / OTR / bracket-blitz took); 0.13.3 redistributed to all 7. Stale rising-tide-sdk-0.13.{0,1,2}.tgz files pruned from each consumer in the same commit.

SDK 0.13.2 (2026-05-22) - s-shelf-sweep amortized batch

Five low-touch SDK items amortized against one rebuild and one tarball distribution. Notable contents: chainhook per-hook auth bearer (chainhook-per-hook-auth spike); sticker layer markers on all 72 SVG templates (rendering-side, no SDK surface change). Distributed to grammata, on-the-record, bracket-blitz only - the 4 rt-* apps remained on 0.13.1 until the next combined bump (0.13.3).

SDK 0.13.1 (2026-05-12) - user-app-association closeout

Closeout of the user-app-association spike. Hardens the linkage between authenticated wallets and apps they have interacted with, via the UserApp join table. Consumers of the 0.13.0 getUserApps / addUserApp / removeUserApp surface now see consistent rows across browse-only and active-creator paths.

SDK 0.13.0 (2026-05-12) - rt-profile spike closeout

Single biggest aggregate-endpoint expansion since the substrate cutover. Adds five aggregate endpoints + matching SDK methods + React hooks + Zod schemas, plus the wallet-connect side effect.

New endpoints + service methods:

  • GET /api/users/address/{stxAddress}/profile -> users.getUserProfile(). Single-call profile aggregate combining user record, links, app participation, and payment totals so rt-profile's overview does not need 5+ round-trips on first paint.
  • GET /api/users/address/{stxAddress}/revenue -> users.getUserRevenue(). Per-product revenue breakdown for the Earnings tab.
  • GET /api/users/address/{stxAddress}/collectors -> users.getUserCollectors(). One row per distinct ownerAddress on the creator's ProductInstance rows, joined with User when an account exists. Powers the Creator Hub audience panel.
  • GET /api/users/address/{stxAddress}/feed -> users.getUserFeed(). Personalized feed: recent published products from creators the user has already collected from. Cursor pagination.
  • GET /api/apps/{id}/trending -> apps.getAppTrending(). Per-app trending feed.

New React hooks (under @rising-tide/sdk/react):

  • useUserProfile, useUserRevenue, useUserCollectors, useUserFeed, useAppTrending.

Wallet-connect side effect:

  • POST /api/users/connect and users.touchAppLink(). Idempotent + throttled. Call on every wallet connect to ensure the calling app is linked to the authenticated wallet, even when the user only browses. Powers cross-app activity aggregation in the rt-profile ProfileRail.

Distribution: SDK 0.12.2 -> 0.13.0 distributed to all 7 SDK consumers (rt-admin, rt-events, rt-landing, rt-profile, grammata, on-the-record, bracket-blitz). All 7 tsc-clean post-bump.

SDK 0.12.2 (2026-05-07) - admin-overhaul closeout

Bundled with the admin-overhaul spike close. Three SDK bumps in one session (0.12.0 -> 0.12.1 -> 0.12.2) collapse into a single canonical 0.12.2 here; only the final tarball is distributed.

New types:

  • ErrorLogStatus (OPEN | INVESTIGATING | RESOLVED) and ErrorLog.status / ErrorLog.resolvedAt fields - error logs are now resolvable through the operator UI
  • TemplatePreviewInnerResponse corrects the TemplatePreviewApiResponse payload shape. The legacy generated.svg field never returned anything; preview.svg is the real path. Both styleSets.generatePreview() and ticketStyles.generatePreview() / generateLivePreview() return this shape consistently
  • SmartProductBase.productStickers - typed embed for ?include=stickers queries (was previously read via as unknown as cast in consumers)
  • ListProtocolEventsParams, ProtocolEventWithRelations - admin-only protocol-event log query
  • ListStorageBlobsParams, StorageStats, StorageProviderName - admin-only storage subsystem visibility
  • PipelineProductTypeEntry, PipelineCompositionResponse - admin-only template-pipeline composition view

New admin services and methods:

  • admin.protocolEvents.list() - paginated query over the canonical ProtocolEvent log (filter by eventType, entityType, entityId, actorAddress, since, until)
  • admin.storage.listBlobs() and admin.storage.getStats() - StorageBlob inventory and aggregate stats (totals, orphans, per-provider breakdown, last-7-day uploads)
  • admin.templates.getPipelineComposition() - read-only registry of which slots/canvases/content-schemas back each product type
  • admin.chainhooks.retryById(id) - manual retry of a failed chainhook from rt-admin (master-key gated)
  • admin.failedEvents.retryBulk(ids) - up to 100 failed events retried sequentially in one call
  • admin.errorLogs.updateStatus(id, status) - flip an error log between OPEN / INVESTIGATING / RESOLVED; resolvedAt stamps automatically

SDK fixes:

  • apps.listApps() no longer double-wrapped on the wire (/api/apps collapsed successResponse({ data: apps }) to successResponse(apps)); the SDK return type is now ApiResponse<App[]> as advertised
  • errorLogs.list() accepts status filter

rt-admin (consumes the above):

  • New /system/protocol-events and /system/storage pages
  • Operational actions on /system/chainhooks (Retry, Enable/Disable, search, status pills) and on the failed-events / error-logs / transactions pages (bulk retry, resolve toggle, summary stats)
  • Contract detail split into admin view (/contracts/[id]) and test harness (/contracts/[id]/test)
  • Real ProtocolEvent activity feed on the dashboard and inside per-entity ItemDetailChainhookCard
  • Pipeline Composition section added to contract detail
  • All eighteen as unknown as casts eliminated by aligning SDK types to actual on-wire shape

Server-side schema:

  • ErrorLog gained status (default OPEN) and resolvedAt (nullable) columns
  • apps route response shape simplified (single-wrap)

SDK 0.11.1 (2026-05-07) - patch

Fixes a latent cross-entry-point context duplication that surfaced as useProtocolUser must be used within a ProtocolUserProvider when useDeployProduct (from @rising-tide/sdk/react) ran inside a tree where the provider was mounted from @rising-tide/sdk/auth. tsup now externalizes the package's own entry-point self-imports, and useDeployProduct imports the auth hook through the package self-reference. Both bundles share the same runtime ProtocolUserContext instance now.

SDK 0.11.0 (2026-05-06) - BREAKING

Schema & storage audit spike CLOSED. Seven phases shipped in one day, smoke-tested on testnet. See the spike folder at rt-protocol/rt-dev-log/dev-spikes/archive/schema-storage-audit/ for per-phase detail.

New types:

  • ActorKind, ProtocolEventType, ProtocolEvent - canonical append-only audit + activity stream that replaces ActivityEvent
  • BlobStatus, UploadStatus, StorageRole, StorageBlob, StorageReference - storage subsystem split into three purpose-built tables
  • CollectionRules - consolidates the five sparse rule columns (fixedRecipient, fixedPriceStx, fixedPriceSbtc, maxItems, allowedEntityTypes) into Collection.rules: Json

Removed (BREAKING):

  • DailyStatsEntry interface and StatsService.getDailyStats() - the underlying DailyStats/HourlyStats tables were never populated; the route never existed in rt-api
  • Tag.usageCount field - sort by attached SmartProductTag count via Prisma _count instead. TagListParams.sortBy narrowed from &apos;name&apos; | &apos;usageCount&apos; | &apos;createdAt&apos; to &apos;name&apos; | &apos;createdAt&apos;
  • Collection.fixedRecipient, fixedPriceStx, fixedPriceSbtc, maxItems, allowedEntityTypes - read and write through Collection.rules now
  • StorageLocation interface renamed to StorageLocationLegacy (response-shape relations on entities still use this shape; the SDK preserves it through the new join). The actual table is gone.

Stats response shape (renames):

  • platform.totalPages to platform.totalProducts
  • platform.totalEditions to platform.totalInstances
  • content.publishedPages to content.publishedProducts
  • content.burnedPages to content.burnedProducts

Server-side:

  • ApiKey.key (plaintext) replaced with ApiKey.keyHash (SHA-256 of pepper || key). Rotation flow returns the freshly minted plaintext once - capture or lose. Pre-mainnet must.
  • New canonical event log: every status-changing service write emits a ProtocolEvent row inside the same transaction. Append-only enforced by Postgres trigger. About 18 dead games and media tables, six Achievement tables, and the ten storage FK columns on SmartProduct and ProductInstance all dropped in the same spike.

SDK 0.7.0 (2026-05-05) - BREAKING

Revenue-split migration spike CLOSED. Drops the deprecated RevenueSplit form-data type and migrates all consumer write surfaces to the substrate-aligned ContributionPickInput shape. Closes inventory items #3-bis (RevenueSplit type drop) and #8 (rt-admin pages-flow off legacy splits[]).

Removed (BREAKING):

  • RevenueSplit interface dropped from @rising-tide/sdk (src/types/index.ts). The @deprecated JSDoc cited the product-contribution-rewrite spike (shipped 0.1.48) as the planned removal point.

Migration map:

Before: RevenueSplit[] = [{ name, address, percentOfTotal, isTemplateArtist?, isAuthor? }, ...]

After: ContributionPickInput = {
  styleSetId?: number,                               // server derives TEMPLATE_ARTIST share from StyleSet.royaltyPercentage
  collaborators?: { address, displayName?, percentage }[],   // free-form contributors
  fundingGroups?: { id, percentage }[],              // registry-backed picks
  stickers?: { stickerId, x, y, scale?, rotation? }[]  // server derives STICKER_ARTIST per sticker
}
// CREATOR / author auto-derives the remainder server-side. No more client-side rebalancing.

Consumer surface changes:

  • rt-admin pages-create + events-create: form state migrated from feeDistributions: FeeDistribution[] to collaborators: CollaboratorInput[] + fundingGroupPicks: FundingGroupAllocation[]. New ContributionPicker component with a working FundingGroup allocator UI (closes inventory item #9). Legacy RevenueSplitDialog / RevenueSplitPieChart / RevenueSplitLegend and the 10-file revenue-split/ component dir deleted.
  • grammata write flow: form state migrated to substrate shape. mapFeeDistributionsToContributions bridge dropped. Internal scratchpad UI for revenue splits removed (per grammata MVP philosophy: "use protocol defaults"). Server-derived TEMPLATE_ARTIST + STICKER_ARTIST + CREATOR rows handle all the math.
  • rt-api boundary cleanup: dead ?include=splits token on /api/events listing dropped (zero consumers, no service implementation). Dead shared/splits.ts payment-creation util dropped (zero consumers). parseContributionPick is now the only revenue-split body parser.

SDK 0.6.11 (2026-05-05)

Server-side sort by currentInstances (Batch I).

  • /api/smart-products and /api/brackets listing routes now accept sortBy=currentInstances. Powers cross-page "Most Signed" / "Most Collected" / "Most Entries" feeds without requiring client-side post-fetch reranking.
  • SmartProductListParams.sortBy SDK union extended to include 'currentInstances'. BracketListParams inherits.
  • OTR upgraded its Most Signed pill from a client-side post-fetch hack (which only sorted within the visible page) to the new server-side sort.

SDK 0.6.10 (2026-05-05)

Tag substrate reconnect Phases 4+5 - consumer wiring + cleanup. Spike CLOSED.

  • rt-admin pages overview gained a Tags column (3 badges + overflow indicator, reads productTags from ?include=tags).
  • grammata Wall PageCards render tag chips below the title; PageDetail renders a tag section at the bottom of the detail view.
  • Dropped stale EventTag SDK interface and Event.tags?: EventTag[] field. Replaced by the polymorphic productTags shape on the ?include=tags payload (same shape used by PAGE / EVENT / PREDICTION).

Closes the 0.6.0 UX regression for tags end-to-end.

SDK 0.6.9 (2026-05-05)

Tag substrate reconnect Phases 1+2+3 - persistence rebuilt as a substrate-aligned polymorphic join.

  • New SmartProductTag Prisma join (composite-unique on (productId, tagId), cascade both directions). Mirrors ProductSticker / ProductContribution shape; works uniformly across PAGE / EVENT / PREDICTION.
  • POST /api/smart-products, /api/events, /api/brackets re-accept body.tags (max 20 names, 64 chars each). Server normalizes (lowercase + trim + whitespace-to-hyphen + strip non-alphanumeric/-/_) and upserts Tag rows by name inside the create transaction. No more client-side two-step "createTag then attach IDs" race.
  • ?include=tags now works on detail and listing routes; SDK SmartProductDetailInclude and SmartProductListInclude unions extended.
  • New SDK type ProductTag and productTags? field on SmartProductBase.
  • /api/tags, /api/tags/popular, /api/tags/[id] registry routes restored (autocomplete, popular suggester, single-fetch).
  • SDK TagsService trimmed to read-only (listTags, getPopularTags, getTagById); useCreateTag React hook removed. Auto-create on submit replaces explicit create.

This is the partial revert of Batches G+H where they crossed into still-wanted shape. Tagging was a wanted feature whose off-chain layer was lost during the 2026-04-26 substrate cutover; the on-chain pageTags pipeline (Clarity (define-constant page-tags ...)) was unaffected and continues to receive consumer-provided tags in parallel.

SDK 0.6.8 (2026-05-05)

Three small follow-up cleanups (Batch H):

  • Dropped now-orphan PageTagSchema and TagSchema exports from @rising-tide/sdk/schemas. Their only internal user (the tags field on PageSchema) was removed in 0.6.7; audit confirmed zero external imports.
  • Dropped dead sdk.tags.createTag block from rt-admin's pages and events create hooks. After 0.6.7 removed the tags: payload, the block populated a variable that was never read again. Tag-input UI components left in place pending a follow-up decision (rip the UI or add SmartProductTag persistence).
  • Fixed 2 known-flaky http-client.test.ts response-interceptor tests. Root cause was async/sync mismatch: the http-client's onRejected is async, so throwing inside it returns a rejected Promise; the test's synchronous try/catch never caught it. Rewrote with Promise.resolve(...).rejects.toMatchObject(...).

Pruned 2 stale substrate-writes.test.ts tests asserting the pageTemplateSetId -> styleSetId mapping that was dropped in 0.6.4. SDK suite: 158/158 pass (was 158/160; +2 fixed flakes, -2 pruned stale tests; net zero failures).

SDK 0.6.7 (2026-05-05) - BREAKING (orphan input drop)

Tech-debt #11 from the 2026-05-04 inventory. Drops the orphan tags: string[] input from the page and event create surface that has been silently discarded at the API boundary since the substrate cutover (2026-04-26).

Removed input fields (BREAKING for consumers passing tags):

  • CreatePageInput.tags (rt-sdk + rt-api service input type)
  • UpdatePageInput.tags (rt-sdk)
  • CreateEventInput.tags (rt-sdk + rt-api service input type)
  • Inline tags?: string[] in the events service createEvent method body
  • tags field in PageSchema and EventSchema Zod schemas
  • POST /api/smart-products and POST /api/events validation blocks + service-call pass-through

Pre-flight audit underestimated scope. Reported zero consumer writers; the actual count was 4 - grammata useCreatePage, on-the-record useCreateOpenLetter, rt-admin pages and events flows. The type-level removal made the silent loss explicit at compile time; consumers were updated in the same pass. Tag asset records (the Tag table) created via sdk.tags.createTag are unaffected.

Sticker.tags is unrelated - asset-side metadata for sticker assets, not page/event tags.

SDK 0.6.6 (2026-05-05)

?include= extension on /api/smart-products (Batch F, tech-debt #4):

  • New: ?include= CSV support on the listing route. Allowed: productType, styleSet. Excludes instances (per-row blow-up) and bracketDetails/pageDetails/contributions/appListings (domain-specific or O(n) on listing).
  • Extended: detail-route allowlist now 8 keys (added styleSet and stickers). Stickers resolves to a nested shape (productStickers: { include: { sticker: { include: { artist: true } } }, orderBy: { position: 'asc' } }) so a single fetch produces the renderable name + artist.
  • New SDK union types: SmartProductListInclude and SmartProductDetailInclude. SmartProductListParams.include?: SmartProductListInclude[]. The SDK CSV-stringifies include before buildQueryParams (the helper serializes arrays as repeated keys; the route parses CSV).
  • grammata PageDetail re-injects styleSet artist + sticker info via the new detail include keys. The useStyleSets() registry-lookup pattern is dropped in favor of direct relation reads.
  • Cache invalidation bitmask realigned to mirror the 8-key detail allowlist exactly. Pre-existing drift fixed: dropped splits (not in any allowlist), added pageDetails + contributions (were already in route allowlist).

The 0.6.5 tarball missed re-exporting the new union types from rt-sdk/src/index.ts; rebuilt as 0.6.6.

SDK 0.6.4 (2026-05-04) - BREAKING

@deprecated SDK markers cleanup (Batch B). Mostly removes deprecated aliases that have had substrate-canonical replacements live for several releases.

Removed:

  • rtClient.pageStyles alias - use rtClient.styleSets
  • pageTemplateSetId SDK param (was alias for styleSetId)
  • Type aliases: PageStyleSet, PageStyle, TicketStyleSet, TicketStyle, TicketStyleSetListParams
  • Event.ticketStyleSet and Event.ticketStyleSetId fields
  • V1 useChainhooks hook
  • startBlock field from V2 chainhook params
  • pageTypeId deprecated alias on StyleListParams

Internal class renames: PageStylesService -> StylesService; PageStyleListParams -> StyleListParams.

Deferred: RevenueSplit form-data type drop (tracked separately as #3-bis - couples with the rt-admin pages-flow migration off legacy splits[]).

SDK 0.6.3 (2026-05-04)

Substrate tech-debt batch (Batch A). Combines 4 of the Top 10 tech-debt items into a single ship.

New substrate routes + SDK methods:

  • GET /api/smart-products/[id]/payments (replaces dead /api/pages/[id]/payments route). SDK method renamed getPagePayments -> getProductPayments. Also dropped dead getEventPayments - the substrate method covers all domains. 4 consumer call sites migrated.
  • GET /api/smart-products/[id]/collections (replaces getCollectionsForPage). SDK method renamed accordingly. Restores OTR campaign resolution.

rt-admin analytics rip: removed analytics state + fetch effect + props from rt-admin pages/[id] AND events/[id]. Both the /api/pages/[id]/analytics and /api/events/[id]/analytics routes were dead. Deleted 2 orphaned chart components.

Other cleanup:

  • getCollectionsForEvent SDK method dropped (unused).
  • pageDetailsService + page-details-service.ts deleted (zero callers).
  • 3 obsolete as unknown as casts dropped (grammata useCollectEdition, OTR useSignLetter, bracket-blitz profile/page.tsx).
  • useBurnInstance orchestration spike formally killed - too shallow + too input-variant for a generic hook.
  • OTR "Most Signed" sort restored via client-side post-fetch sort on currentInstances.

SDK 0.6.1 (2026-05-04)

Substrate tech-debt batch. Three slices: route lift, Tier 1 relation re-injection, and minor cleanup.

/api/pages route dropped (rt-api):

POST/PATCH/DELETE handlers lifted into /api/smart-products and /api/smart-products/[id]. mapSmartProductToLegacyPage mapper deleted along with 4 dead tests. SDK createPage/updatePage/deletePage retargeted to the substrate URLs. pageService trimmed to PAGE-domain create only.

Tier 1 relation re-injection: cached useProductTypes() / useStyleSets() lookups in rt-admin all-pages, grammata PageCard, and grammata PageDetail. Replaces #N type/template badges with displayName; restores style-artist name on PageDetail via a new server-side artist include on /api/styles/pages.

Cleanup: dropped deprecated pageStyleSetId from SDK Zod schema. Migrated OTR's useProductTypeId hook out of existence (StylePickerDialog calls productTypeSlug directly).

SDK 0.6.0 (2026-05-04) - BREAKING

Final cut of the legacy services drop. PagesService removed, legacy Page / Edition / PageType types deleted. All consumer surfaces now operate on substrate-canonical types (SmartProductPage / ProductInstance / ProductType).

Removed types (BREAKING):

  • Page - replaced by SmartProductPage from @rising-tide/sdk
  • Edition - replaced by ProductInstance
  • PageType - replaced by ProductType
  • PageAppListing, PageSticker, PageTag, PageListParams - dropped (substrate listing rows don't embed those relations)
  • PageStatus - now an alias for ProductStatus (same union plus LOCKED / SETTLED)

Removed services (BREAKING):

  • rtClient.pages - all five methods migrated:
    • pages.listPages -> smartProducts.list({ domain: 'PAGE' })
    • pages.getPageById -> smartProducts.getById
    • pages.createPage -> smartProducts.createPage
    • pages.updatePage -> smartProducts.updatePage
    • pages.deletePage -> smartProducts.deletePage
  • rtAdmin.pages removed for the same reason

Field rename map (consumer migration cookbook):

Legacy fieldSubstrate field
page.author (string)page.ownerAddress
page.authorName (string?)resolve via user lookup
page.editionsPublished / page.currentEditionspage.currentInstances
page.maxEditionspage.maxInstances (now number | null)
page.content (JsonObject)(page.productConfig as Record<string, unknown>).content
page.pageTypeIdpage.productTypeId
page.pageType (relation)not embedded - separate productTypes.getById fetch
page.editions[] (joined)separate smartProducts.getInstances(productId) fetch
page.appListings[] (joined)separate smartProducts.getListedApps(productId) fetch
page.pageStyleSetIdpage.styleSetId
page.styleSet (relation)not embedded - separate styleSets.getById fetch
page.sourceApp (relation)not embedded - separate apps.getById fetch
page.tags[] (joined)not on substrate (deferred follow-up)
edition.editionNumber (string)instance.instanceNumber (string)
edition.editionId (string)instance.instanceId (string)
edition.pageIdinstance.productId
edition.metadata (Record)instance.instanceConfig (Record)
edition.currencyType (string)now narrowed to 'STX' | 'SBTC'

Transaction builder updates:

PublishEditionTransactionParams, PublishOpenLetterTransactionParams, ListPageTransactionParams, SetPageInfoTransactionParams all updated from page: Page to page: SmartProductPage. Same fields read by the builders (minFeeStx, minFeeSbtc, contractAddress, contractName) - just the param type signature changes.

UX regressions (re-add via separate fetches in a follow-up release):

  • Page type and style set names appear as numeric IDs on listings until productTypes / styleSets resolution is wired back in
  • Style artist attribution dropped on PageDetail / EditionDetail
  • Sticker info dropped on PageDetail
  • Tags dropped on PageDetail / page-overview
  • Per-collector aggregation simplified on profiles (no editions[] join on substrate listing rows)
  • "Most Signed" sort dropped from OTR browse (no currentInstances substrate sortBy)

rt-api: unchanged for this ship. The /api/pages route still exists and is hit by smartProducts.createPage/updatePage (substrate-named SDK methods, legacy URL). Folding into substrate-canonical routes is queued as a follow-up.

SDK 0.5.2 (2026-05-04)

Substrate filter additions on /api/smart-products:

  • productTypeSlug filter (resolves to productTypeId server-side via the cached ProductType registry; unknown slugs return empty list)
  • collectionId filter (joins via the polymorphic CollectionItem table; ids are 2-step resolved server-side)

Both fields added to SmartProductListParams. No consumer migration required - all 7 consumer apps continue to work unchanged. Future deprecation/removal of the legacy /api/pages sugar routes (queued as active/sdk-legacy-services-drop/ Phase 2 final) can use these filters as their substrate-canonical replacement.

SDK 0.5.1 (2026-05-04) - BREAKING

Phase 2 of active/sdk-legacy-services-drop/: deep cleanup of legacy artifacts. Removes two entire services, six dead-route methods, four dead React hook files, two dead type aliases, and one sugar route. Required UI refactor in rt-admin's PageTypeDialog.

Removed services (BREAKING):

  • rtClient.editions (EditionsService class) - all methods hit dead /api/editions/* routes. Migrate to rtClient.productInstances and rtClient.users.getUserInstances.
  • rtClient.pageTypes (PageTypesService class) - 4 of 5 methods hit dead routes; the surviving listPageTypes was sugar over substrate. Migrate to rtClient.productTypes.

Trimmed PagesService:

  • Dropped: getPageEditions, getListedApps, listOnApps, removeAppListing, getTournamentLeaderboard, getPageContributions, plus the deprecated aliases publishToApps, getPageApps, removePageFromApp. All hit dead routes; substrate replacements documented in 0.4.1 changelog. Surviving methods: listPages, getPageById, createPage, updatePage, deletePage (still hit live /api/pages sugar routes; full removal queued for 0.6.0).

Removed React hooks:

  • useEditions.ts file (entirely dead, no exports were live)
  • usePageTypes.ts file (only usePageType was re-exported; that hit a dead route)
  • Dead implementations in usePages.ts (kept only the 4 app-listing shims, now hitting substrate)

Removed types:

  • EditionListParams, EditionLookupByPage, EditionLookupByContract, EditionLookupParams - only used by dropped EditionsService
  • PageTypeListParams - only used by dropped PageTypesService
  • WritingProductType (alias for PageProductType)
  • WritingContractProcessRequest (alias for PageContractProcessRequest)

Removed rt-api routes:

  • /api/page-types (GET sugar) - no callers remain after consumer migration to productTypes.list({ domain: 'PAGE' })

Consumer migrations:

  • All 7 editions.listEditions call sites migrated to users.getUserInstances (rt-profile x5, OTR, grammata)
  • rt-admin PageTypeDialog refactored: form now takes substrate fields (slug + displayName + label) with auto-derived slug on create
  • rt-admin settings/page.tsx page-types tab fetches via productTypes.list filtered to PAGE domain
  • grammata useCollectionFilters migrated from EditionListParams to UserInstancesParams (drops search, minPrice, maxPrice filter fields - substrate doesn't support them server-side)
  • grammata usePageTypes rewritten to call productTypes.list and shape items back to legacy PageType for downstream UI compat
  • Boundary casts (as unknown as Edition[]) at 4 sites where consumer UI still expects the legacy Edition shape; deferred to 0.6.0 with the full Page/Edition UI type refactor

Dropped service exports from RisingTideAdmin:

  • rtAdmin.editions, rtAdmin.pageTypes removed from the admin SDK

Net delta: -1280 LOC across SDK + consumers. SDK build clean, all 7 consumer apps tsc clean, rt-api tsc clean.

SDK 0.4.1 (2026-05-03)

Combined ship of two follow-up spikes from the morning's 0.2.0 release: orchestration hooks (active/sdk-orchestration-hooks/) + legacy-drop Phase 1 (active/sdk-legacy-services-drop/). The orchestration hook absorbs pages.createPage deploy-flow callers, which is why both spikes ship together.

New: useDeployProduct orchestration hook

Generic deploy-flow hook in @rising-tide/sdk/react. Handles chainhook register/cleanup, Privy/BYOW signing dispatch, step state machine, and cleanup ordering. Consumer pre-generates the contract code and provides createDraft, patchDeployTxId, deleteDraft callbacks plus an optional onAfterDeploy slot. Returns typed step (idle | registering | creating | deploying | updating | done | error) plus a requiresManualRecovery flag for the deploy-broadcast-but-patch-failed case.

Net consumer footprint reduction: -496 LOC across grammata + on-the-record + bracket-blitz (1046 -> 550 LOC); +257 LOC of new SDK hook code. Per-domain orchestration hooks (useDeployPageProduct etc.) were considered and rejected: a single generic hook with consumer-provided callbacks gives the same DX win without bloating the SDK surface.

Phase 1 of legacy services drop: bug fix + new substrate surface

The 2026-04-26 legacy-table-drop deleted /api/editions/* and the /api/pages/[id]/{apps,list,editions} subroutes from rt-api but left their callers in place on the SDK + consumer apps. Several consumer flows had been silently 404'ing in production for a week. This release fixes that.

New substrate routes:

  • GET/POST/DELETE /api/smart-products/[id]/apps (replaces dead /api/pages/[id]/apps + /list)
  • GET /api/product-instances/lookup (replaces dead /api/editions/lookup; productId or contractAddress+contractName modes)
  • GET /api/smart-products/[id]/instances?instanceNumber=N (single-row lookup)
  • DELETE /api/product-types/[slug] (rejects 409 IN_USE when SmartProducts still reference the type)

New SDK methods:

  • rtClient.smartProducts.getListedApps(productId), listOnApps(productId, appIds), removeAppListing(productId, appId)
  • rtClient.productInstances.findByInstanceNumber(productId, instanceNumber)
  • rtClient.productInstances.findByContractAndInstanceNumber({ contractAddress, contractName, instanceNumber })
  • rtClient.productTypes.delete(slug)
  • UpdatePageInput.deployTxId field added

Deprecated (still functional, slated for removal):

  • rtClient.pages - use rtClient.smartProducts (per-method substrate map in PagesService JSDoc)
  • rtClient.editions - use rtClient.productInstances and rtClient.users.getUserInstances (per-method map in EditionsService JSDoc)
  • rtClient.pageTypes - use rtClient.productTypes (per-method map in PageTypesService JSDoc)
  • rtClient.pageStyles - use rtClient.styleSets (same service instance, substrate-aligned name)

Methods on the deprecated services that hit dead routes (pages.getListedApps/listOnApps/removeAppListing/getPageEditions, editions.getEditionByNumber/getEditionById/listEditions) silently 404 against production; migrate to the substrate equivalents in the JSDoc.

Full removal of the four legacy service classes is queued as Phase 2 of sdk-legacy-services-drop - it requires UI field-rename refactors (Page->SmartProduct, Edition->ProductInstance, PageType->ProductType) across ~30 remaining call sites and is tracked separately.

SDK 0.3.0 (2026-05-03 - SUPERSEDED by 0.4.1)

Intermediate pack during the 2026-05-03 session; superseded by 0.4.1 which bundles the orchestration-hook spike. Functionally identical to 0.4.1 minus the useDeployProduct hook + UpdatePageInput.deployTxId field. No consumer ships on 0.3.0.

New substrate routes:

  • GET/POST/DELETE /api/smart-products/[id]/apps (replaces the dead /api/pages/[id]/apps + /list triple)
  • GET /api/product-instances/lookup (replaces dead /api/editions/lookup; productId or contractAddress+contractName modes)
  • GET /api/smart-products/[id]/instances?instanceNumber=N (single-row lookup, replaces legacy /api/editions/lookup pageId form)
  • DELETE /api/product-types/[slug] (rejects 409 IN_USE when SmartProducts still reference the type)

New SDK methods:

  • rtClient.smartProducts.getListedApps(productId), listOnApps(productId, appIds), removeAppListing(productId, appId)
  • rtClient.productInstances.findByInstanceNumber(productId, instanceNumber)
  • rtClient.productInstances.findByContractAndInstanceNumber({ contractAddress, contractName, instanceNumber })
  • rtClient.productTypes.delete(slug)

Deprecated (still functional, slated for removal):

  • rtClient.pages - use rtClient.smartProducts (per-method substrate map in PagesService JSDoc)
  • rtClient.editions - use rtClient.productInstances and rtClient.users.getUserInstances (per-method substrate map in EditionsService JSDoc)
  • rtClient.pageTypes - use rtClient.productTypes (per-method substrate map in PageTypesService JSDoc)
  • rtClient.pageStyles - use rtClient.styleSets (same service instance, substrate-aligned name)

Methods on the deprecated services that hit dead routes (pages.getListedApps/listOnApps/removeAppListing/getPageEditions, editions.getEditionByNumber/getEditionById/listEditions/burnEdition/featureEdition) silently 404 against production; migrate to the substrate equivalents listed in their JSDoc.

SDK 0.2.0 (2026-05-03) - BREAKING

Major-version cut completing the substrate hook adoption (dev-spikes/completed/sdk-substrate-hook-adoption/).

Breaking changes:

  • Removed legacy React hooks from @rising-tide/sdk/react: usePages, usePage, useEditions, useEdition, usePageTypes, usePageStyleSets, useCreatePage, useUpdatePage, useDeletePage, useUpdateEdition, useBurnEdition, useDeleteEdition. Use the substrate-shape replacements: useSmartProducts, useSmartProduct, useProductInstances, useProductInstance, useProductTypes, useStyleSets. For mutations, call the rtClient.smartProducts methods directly inside your own mutation wrapper - createPage / updatePage / deletePage, their event and prediction equivalents, and publishPrediction - plus rtClient.productInstances.burn.

Still exported (legacy services kept):

  • rtClient.pages, rtClient.editions, rtClient.pageTypes, rtClient.pageStyles services remain alongside rtClient.smartProducts / rtClient.productInstances / rtClient.productTypes / rtClient.styleSets. Niche methods without substrate equivalents (e.g. rtClient.pages.getListedApps, rtClient.editions.featureEdition, rtClient.pageStyles.validateStyle) remain on the legacy namespaces. Substrate-side replacements for the remaining methods are tracked as a future spike.
  • Specific style-CRUD and app-listing hooks remain exported under their legacy names: useListPageOnApps, useGetListedApps, useRemoveAppListing, usePageEditions, usePageType, usePageStyleSet, useCreatePageStyleSet, useUpdatePageStyleSet, useDeletePageStyleSet, useGenerateStylePreview, useGenerateLiveStylePreview, useGenerateStyle, useValidateStyle.

SDK 0.1.58 (2026-05-03)

useUserInstances cross-domain hook + new public API route.

  • New React hook useUserInstances(stxAddress, params?) returns paginated ProductInstance rows owned by an address, joined with parent SmartProduct + ProductType for unified rendering across domains
  • New SDK service method rtClient.users.getUserInstances(stxAddress, params?) backed by new public route GET /api/users/address/{stxAddress}/instances
  • Filters: domain (PAGE | EVENT | PREDICTION), status, parentProductId, sortBy, sortOrder, page, limit
  • New input type UserInstancesParams exported from @rising-tide/sdk

SDK 0.1.57 (2026-05-03)

Substrate write surface on rtClient.smartProducts and rtClient.productInstances. Phase 1 of dev-spikes/completed/sdk-substrate-hook-adoption/.

  • New rtClient.smartProducts.createPage / updatePage / deletePage (POST/PUT/DELETE /api/pages)
  • New rtClient.smartProducts.createEvent / updateEvent / deleteEvent (POST/PUT/DELETE /api/events)
  • New rtClient.smartProducts.createPrediction / updatePrediction / deletePrediction / publishPrediction (POST/PATCH/DELETE /api/brackets + POST /api/smart-products/{id}/publish)
  • New rtClient.productInstances.burn(productId, instanceId, { burnTxId }) records burn pending state. Off-chain only - does NOT sign or broadcast the on-chain burn (use rtClient.privy.signBurnEdition or Stacks Connect for that)
  • New API route PATCH /api/product-instances/{id}/burn (substrate replacement for the dropped legacy /api/editions/{id}/burn)
  • New input types exported: CreatePageInput, UpdatePageInput, CreateEventInput, UpdateEventInput, CreatePredictionInput, UpdatePredictionInput, BurnProductInstanceInput

SDK 0.1.56 (2026-05-03)

Substrate-shaped hook layer + auth composition. Additive only - all legacy hooks and services remain exported.

  • New React hooks: useSmartProducts, useSmartProduct, useSmartProductContributions, useProductInstances, useProductInstance, useProductTypes, useProductType, useStyleSets, useStyleSet, useFundingGroups, useFundingGroup, useUserActivity, useInvites, useInvitePreview (under @rising-tide/sdk/react)
  • New auth exports under @rising-tide/sdk/auth: RisingTideAuthProvider (composes AuthMethodProvider + AppModeProvider + ModeGate + ConditionalProviders + ProtocolUserProvider), generic AuthGate route guard with allowlist / customMatcher / requiredEntitlement props, useEntitlement(verb) shorthand
  • New RisingTideQueryProvider re-export (alias of RisingTideProvider)
  • rtClient.instances and rtClient.styleSets namespace aliases (resolve to productInstances / pageStyles)
  • next added as optional peer dep (^15) for next/navigation consumed by SDK AuthGate
  • rt-profile migrated to SDK AuthGate; local AuthGate.tsx reduced to a route-matcher helper

Consumer migration off legacy hooks was tracked in spike dev-spikes/completed/sdk-substrate-hook-adoption/ and shipped in 0.2.0.

SDK 0.1.53 (2026-05-01)

  • Substrate hardening: slug compound unique @@unique([domain, slug])
  • ContractTemplate.appId enforcement in evaluateTemplateScope
  • FundingGroup routes hardened (Zod + transition guards)
  • PATCH escape hatch closed (rtClient.users.updateAppPreferences)
  • Mainnet seed UUIDs runtime-generated with ALLOW_MAINNET_SEED guard
  • Sticker substrate: ProductSticker join, composeStickerLayer in renderSmartProduct
  • Per-binding StyleSet royalty via StyleSetProductType.royaltyPercentage

SDK 0.1.48 (2026-04-30)

  • Product Contribution Rewrite complete
  • ContributionRole enum: all six roles
  • ProductContribution schema with position freezing
  • Two-tier allocation (creator-allocated vs protocol-routed)

SDK 0.1.41 (2026-04-29)

  • AuthGate for rt-profile with allowlist
  • AuthenticationChoice: both Privy and BYOW offered in all modes

SDK 0.1.35 (2026-04-27)

  • Page/Event mappers expose productType: { id, slug, displayName, domain }
  • Routes accept productTypeSlug (wins over legacy id; cross-domain blocked)

SDK 0.1.32 (2026-04-26)

  • Page + Event substrate migration complete
  • Dual-write era live

SDK 0.1.28 (2026-04-24)

  • Invites first-class: Invite entity with 5-state lifecycle
  • 60-bit Crockford Base32 codes with CSPRNG
  • Role-on-claim support
  • Mint quotas by role

On this page

sdk 0.14.165 + ui 0.7.194 (2026-10-17) - one wallet call per sign-in, and a payee may have no namesdk: Payment.recipientName may be nullui: the wallet is asked for once per sign-insdk 0.14.164 + ui 0.7.193 (2026-10-17) - a bomb may be priced at 0sdk: use-bomb at amount 0uisdk 0.14.163 + ui 0.7.192 (2026-10-05) - the pocket is a delegate walletsdk: rtClient.delegateWalletuisdk 0.14.162 + ui 0.7.191 (2026-10-05) - a pocket, and products that act for the personsdk: rtClient.pocketsdk: bracket and game transactions name the personui: the displayed address is the stored walletsdk 0.14.161 + ui 0.7.190 (2026-10-04) - products deploy at Clarity 6sdk: PRODUCT_CLARITY_VERSIONui: useDeployProductui 0.7.189 (2026-10-03) - the hub installs as a glyphui: UserAppRailui 0.7.188 (2026-10-03) - the push ask shows only when push can be sentui: usePushSubscriptionui 0.7.187 (2026-09-30) - the sign-in dialog says what it doesui: AuthenticationChoice copysdk 0.14.160 + ui 0.7.186 (2026-09-30) - a new sticker starts at the template's spotsdk: defaultPlacement reads the zone's anchorui: no change of its ownsdk 0.14.159 + ui 0.7.185 (2026-09-28) - bitcoin reads in satssdk: sats at every sizeui: sats in every component that shows bitcoinsdk 0.14.158 + ui 0.7.184 (2026-09-26) - a tournament from its contract, and Web Pushsdk: one authority for a tournamentsdk: a round, countedsdk: Web Push subscriptions, and a push tap can be countedui: usePushSubscription, and RisingTideProviders records push tapsconformance 0.11.2 (2026-10-04) - the tree carries only the tarballs it installsconformance 0.11.1 (2026-09-25) - an unrelated comparison is not a gateconformance 0.11.0 (2026-09-25) - the push surfacesdk 0.14.155 (2026-09-25) - a record for one gameui 0.7.181 (2026-09-25) - lockstep with sdk 0.14.155sdk 0.14.154 (2026-09-25) - a leaderboard per gameui 0.7.180 (2026-09-25) - lockstep with sdk 0.14.154sdk 0.14.153 (2026-09-24) - a claimed invite reads as claimedui 0.7.179 (2026-09-24) - lockstep with sdk 0.14.153sdk 0.14.152 (2026-09-24) - open spots on the waitlistui 0.7.178 (2026-09-24) - lockstep with sdk 0.14.152sdk 0.14.151 (2026-09-23) - a trophy drawn from a sample matchui 0.7.177 (2026-09-23) - lockstep with sdk 0.14.151sdk 0.14.150 (2026-09-21) - a seat can leave a lobbyui 0.7.176 (2026-09-21) - lockstep with sdk 0.14.150conformance 0.10.1 (2026-09-21) - the affordability matcher, corrected against the estateconformance 0.10.0 (2026-09-21) - a paid control must know the user can paysdk 0.14.149 / ui 0.7.175 (2026-09-21) - one primitive for every paid buttonsdk 0.14.148 / ui 0.7.174 (2026-09-20) - a deploy is priced by its own sizesdk 0.14.147 / ui 0.7.173 (2026-09-20) - what a contract call costs, in one placesdk 0.14.146 / ui 0.7.172 (2026-09-20) - the match read carries its claim transactionsdk 0.14.145 / ui 0.7.171 (2026-09-20) - nothing signs in allow modesdk 0.14.144 / ui 0.7.170 (2026-09-20) - the bomb is a spectator itemsdk 0.14.143 / ui 0.7.169 (2026-09-20) - a host can sit at their own tablesdk 0.14.141 / ui 0.7.167 (2026-09-20) - the ladder is climbed in order, and it reaches the gamessdk 0.14.140 / ui 0.7.166 (2026-09-19) - playing a lobby, and a player's recordsdk 0.14.139 / ui 0.7.165 (2026-09-19) - the GAME domain and game lobbiesui 0.7.164 (2026-09-16) - one shellsdk 0.14.138 / ui 0.7.163 (2026-09-15) - a renewal invite can only be spent on the poolsdk 0.14.137 / ui 0.7.162 (2026-09-15) - the waitlist pool, and a pick that never sees a contactconformance 0.9.7 (2026-09-14) - share cards and chain-bound text get their rulesui 0.7.161 (2026-09-12) - a refused display name reaches the person who typed itui 0.7.160 (2026-09-11) - every app renders its page on the server againsdk 0.14.136 / ui 0.7.159 (2026-09-10) - non-ASCII no longer reaches the chain silentlyui 0.7.158 (2026-09-10) - the curtain waits for the tokenui 0.7.157 (2026-09-10) - the entry curtain is universal, and it stops asking the catalogui 0.7.156 (2026-09-10 - SUPERSEDED by 0.7.157)sdk 0.14.135 + ui 0.7.155 (2026-09-09) - the touch floor follows the finger, and the shell gets a tabletconformance 0.9.6 (2026-09-09) - a floor released by width is a defectsdk 0.14.133 + ui 0.7.153 (2026-09-06) - the protocol has no storage walletsdk 0.14.132 + ui 0.7.152 (2026-09-06) - buying more room, behind a flagsdk 0.14.131 + ui 0.7.151 (2026-09-05) - a render is not a destination writesdk 0.14.130 + ui 0.7.150 (2026-09-05) - what the first Save As walk foundsdk 0.14.129 + ui 0.7.149 (2026-09-05) - Save As, under the owner's keysdk 0.14.128 + ui 0.7.146 (2026-09-05) - saved by defaultsdk 0.14.127 + ui 0.7.145 (2026-09-04) - an invite to give announces itselfsdk 0.14.126 + ui 0.7.144 (2026-09-04) - achievements carry artsdk 0.14.125 + ui 0.7.143 (2026-09-04) - an invite has a creator, not an ownerui 0.7.142 (2026-09-04) - a tab can carry a countsdk 0.14.124 + ui 0.7.140 (2026-09-04) - the feed can say somebody joinedsdk 0.14.123 + ui 0.7.139 (2026-09-04) - a notification says who did itsdk 0.14.122 + ui 0.7.138 (2026-09-03) - events get a public hook surfacesdk 0.14.121 + ui 0.7.137 (2026-09-03) - two fields nothing could readconformance 0.9.4 (2026-09-03) - a marker split across a line wrap is still a markerui 0.7.136 (2026-09-03) - arrival is Google or Xsdk 0.14.120 + ui 0.7.135 (2026-09-02) - the ladder as a mapsdk 0.14.119 + ui 0.7.134 (2026-09-02) - the shelf and the next rungsdk 0.14.118 + ui 0.7.133 (2026-09-02) - the rungs paysdk 0.14.117 + ui 0.7.132 (2026-08-31) - a display step, and dead config removedsdk 0.14.116 + ui 0.7.131 (2026-08-31) - the operator half of the waitlistsdk 0.14.115 + ui 0.7.130 (2026-08-31) - a stranger can ask to be let insdk 0.14.114 + ui 0.7.129 + conformance 0.9.3 (2026-08-31) - a queued faucet tap has a name, and a fork gets gradedui 0.7.128 (2026-08-30) - contact fields stop leaving the API, and a skipped name is distinguishableconformance 0.9.2 (2026-08-29) - comment hygiene ships as an opt-in setsdk 0.14.113 + ui 0.7.127 (2026-08-29) - a ticket knows when its event startssdk 0.14.112 + ui 0.7.126 (2026-08-29) - comments state the presentsdk 0.14.111 + ui 0.7.125 (2026-08-27) - the frozen cap table gets a typesdk 0.14.110 + ui 0.7.124 (2026-08-26) - the social-card render becomes reachableconformance 0.9.1 (2026-08-26) - the count the guard writes reaches zeroui 0.7.123 (2026-08-26) - clients read the create fee instead of carrying itsdk 0.14.109 + ui 0.7.121 (2026-08-26) - creating is priced in the marketplace's own tokenconformance 0.9.0 (2026-08-26) - credits stop being a unit anywhereui 0.7.120 (2026-08-25) - a sticker fills its placement box, whatever its markupsdk 0.14.108 + ui 0.7.119 (2026-08-25) - a series holds assets, not just templatesui 0.7.118 (2026-08-24) - nothing the package ships renders below the floorsdk 0.14.107 + ui 0.7.116 (2026-08-24) - show the thing, and let the platform open itconformance 0.8.0 (2026-08-24) - the money rules ship narrowed, on purposeconformance 0.7.0 (2026-08-24) - the launch rule reverses, and it is a real reversalsdk 0.14.106 + ui 0.7.115 (2026-08-24) - the app registry says what an item is calledconformance 0.6.0 (2026-08-24) - the registry is the routing table, and a launch is not a linkui 0.7.113 (2026-08-24) - feed the rule Privy's own status, not a mirror of itui 0.7.112 (2026-08-24) - ask what is known, not how far the machinery gotui 0.7.111 (2026-08-24) - resolution waits for the address, not just for Privyui 0.7.110 (2026-08-24) - "not signed in" and "not known yet" are different answerssdk 0.14.105 + ui 0.7.108 (2026-08-23) - a delivery is not a collectsdk 0.14.104 + ui 0.7.107 (2026-08-23) - what happened while you were awaysdk 0.14.103 + ui 0.7.106 (2026-08-21) - how much can actually go on this templatesdk 0.14.102 + ui 0.7.105 (2026-08-21) - a template can tell you it is hard to readsdk 0.14.101 + ui 0.7.104 (2026-08-21) - a canvas names a slot, not a sizesdk 0.14.100 + ui 0.7.103 (2026-08-21) - the SDK stops choosing an orientation for yousdk 0.14.99 + ui 0.7.102 (2026-08-17) - an invite can be held by someone who did not mint itsdk 0.14.98 + ui 0.7.101 (2026-08-17) - sBTC balances stop reading 0sdk 0.14.97 + ui 0.7.100 (2026-08-16) - the tags trim rolled backsdk 0.14.96 + ui 0.7.99 (2026-08-16) - the types stop lyingsdk 0.14.95 + ui 0.7.98 (2026-08-15) - the operational mode becomes flippable without a redeploysdk 0.14.94 + ui 0.7.97 (2026-08-15) - the archival surface first shipssdk 0.14.93 + ui 0.7.96 (2026-08-14) - moderation gets a historysdk 0.14.92 + ui 0.7.94 + conformance 0.5.1 (2026-08-14) - the testnet faucets become the protocol'ssdk 0.14.91 + ui 0.7.93 (2026-08-13) - the SDK builds on machines that are not oursui 0.7.92 + conformance 0.5.0 (2026-08-13) - the design system becomes an artifactconformance 0.4.0 (2026-08-13) - a rule is deleted, and warn stops being a place to parkconformance 0.3.0 (2026-08-13) - the rules an app can check for itselfui 0.7.88 (2026-08-12) - apps stop polling when nobody is using themSDK 0.14.89 (2026-08-11) - every broadcast now says whether it owes an eventSDK 0.14.88 (2026-08-11) - a second route in, and a signal that watches itSDK 0.14.87 (2026-08-10) - two health signals for a predicate that was never thereui 0.7.78 (2026-08-08) - Privy 3.36 across the estateSDK 0.14.83 - 0.14.84 (2026-08-06/07) - one composition record, refusal over silence, one mintSDK 0.14.82 (2026-08-06) - attributes, and one vocabulary across every productSDK 0.14.81 (2026-08-05) - app-scoped style sets and per-product artui 0.7.61 - 0.7.75 (2026-07-30 - 2026-08-05) - the shared-surface estate passSDK 0.14.79 - 0.14.80 (2026-07-29/30) - honest profile cards; the design floorSDK 0.14.74 - 0.14.78 (2026-07-29) - explicit generate modes; the SDK becomes the contractSDK 0.14.68 - 0.14.73 (2026-07-28) - the EVENT domain grows upSDK 0.14.64 - 0.14.67 (2026-07-27/28) - one revenue path, and the mirrors that stay trueSDK 0.14.62 - 0.14.63 (2026-07-23) - invites locked to a verified identitySDK 0.14.59 - 0.14.61 (2026-07-22) - per-app access grantsSDK 0.14.58 (2026-07-21) - invite mint authoritySDK 0.14.57 (2026-07-21) - honest rate-limit back-offSDK 0.14.55 - 0.14.56 (2026-07-06) - invite gating simplified; type-mirror syncSDK 0.14.54 (2026-07-04) - create-to-earn: STX-payable createSDK 0.14.53 (2026-07-04) - multi-app invite previewSDK 0.14.52 (2026-07-03) - funded invitesSDK 0.14.51 (2026-07-01) - letter identity read modelSDK 0.14.50 (2026-07-01) - edition provenance (minter)SDK 0.14.49 (2026-07-01) - author name and letter salutationSDK 0.14.48 (2026-06-30) - capabilities on the bracket (PREDICTION) inputsSDK 0.14.47 (2026-06-30) - capabilities on the EVENT inputsSDK 0.14.46 (2026-06-30) - self-serve product compositionSDK 0.14.31 - 0.14.32 (2026-06-19) - detachable capability re-gated to the edition ownerSDK 0.14.30 (2026-06-18) - substrate health surfaceSDK 0.14.25 - 0.14.29 (2026-06-17) - detachable capability invocation surfaceSDK 0.14.22 - 0.14.24 (2026-06-15) - rt-studio multi-canvas edit and brand setsSDK 0.14.21 (2026-06-14) - per-asset earningsSDK 0.14.19 - 0.14.20 (2026-06-14) - rt-studio approval workflow and template authoringSDK 0.14.17 - 0.14.18 (2026-06-14) - rt-studio writable asset surface and per-asset usageSDK 0.14.16 (2026-06-13) - rt-admin observability surfaceSDK 0.14.14 - 0.14.15 (2026-06-12) - contract-template retirementSDK 0.14.12 - 0.14.13 (2026-06-11) - currency policy and preferenceSDK 0.14.11 (2026-06-10) - retire dead stats and leaderboards surfaceSDK 0.14.10 (2026-06-10) - remove orphaned Contract typesSDK 0.14.9 (2026-06-10) - SmartProduct catalog metadataSDK 0.14.8 (2026-06-10) - contract identity denormalizedSDK 0.14.5 - 0.14.7 (2026-06-10) - rt-admin protocol resyncSDK 0.14.4 (2026-06-06) - cron operator surfaceSDK 0.14.3 (2026-06-05) - sentiment-letter recipesSDK 0.14.2 (2026-06-04) - sentiment snapshotSDK 0.14.1 (2026-06-03) - composable capabilitiesSDK 0.14.0 (2026-06-03) - headless SDK plus @rising-tide/uiSDK 0.13.24 (2026-06-02) - transaction polling fixSDK 0.13.23 (2026-06-02) - actor taxonomy renameSDK 0.13.22 (2026-06-02) - ProtocolEvent stream expansionSDK 0.13.21 (2026-06-01) - reorg rollback: burn and deploy orphansSDK 0.13.20 (2026-06-01) - reorg rollback: mint orphansSDK 0.13.19 (2026-06-01) - pending-transaction lifecycle simplifiedSDK 0.13.18 (2026-05-31) - pending-transaction admin surfaceSDK 0.13.15 (2026-05-31) - pending-transaction reconciliationSDK 0.13.14 (2026-05-30) - substrate trifecta closeoutSDK 0.13.13 (2026-05-29) - ConditionalProviders structural refactorSDK 0.13.11 - 0.13.12 (2026-05-28) - chainhook + protocol-event tighteningSDK 0.13.10 (2026-05-28) - chainhook re-register type tighteningSDK 0.13.9 (2026-05-27) - generateContractCode callbackSDK 0.13.3 (2026-05-25) - CreatorsSupported aggregateSDK 0.13.2 (2026-05-22) - s-shelf-sweep amortized batchSDK 0.13.1 (2026-05-12) - user-app-association closeoutSDK 0.13.0 (2026-05-12) - rt-profile spike closeoutSDK 0.12.2 (2026-05-07) - admin-overhaul closeoutSDK 0.11.1 (2026-05-07) - patchSDK 0.11.0 (2026-05-06) - BREAKINGSDK 0.7.0 (2026-05-05) - BREAKINGSDK 0.6.11 (2026-05-05)SDK 0.6.10 (2026-05-05)SDK 0.6.9 (2026-05-05)SDK 0.6.8 (2026-05-05)SDK 0.6.7 (2026-05-05) - BREAKING (orphan input drop)SDK 0.6.6 (2026-05-05)SDK 0.6.4 (2026-05-04) - BREAKINGSDK 0.6.3 (2026-05-04)SDK 0.6.1 (2026-05-04)SDK 0.6.0 (2026-05-04) - BREAKINGSDK 0.5.2 (2026-05-04)SDK 0.5.1 (2026-05-04) - BREAKINGSDK 0.4.1 (2026-05-03)SDK 0.3.0 (2026-05-03 - SUPERSEDED by 0.4.1)SDK 0.2.0 (2026-05-03) - BREAKINGSDK 0.1.58 (2026-05-03)SDK 0.1.57 (2026-05-03)SDK 0.1.56 (2026-05-03)SDK 0.1.53 (2026-05-01)SDK 0.1.48 (2026-04-30)SDK 0.1.41 (2026-04-29)SDK 0.1.35 (2026-04-27)SDK 0.1.32 (2026-04-26)SDK 0.1.28 (2026-04-24)