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 apushlistener that callsshowNotificationinsideevent.waitUntil, and reads the payload'sfallbackfor kinds it does not know. Safari revokes push permission from a site whose push shows nothing.push-worker-handles-taps: anotificationclicklistener that focuses or opens the app.push-unsubscribes-on-sign-out: an app that subscribes also callsdisable(), 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 writtenuseState<string>; the unspaced form read every type parameter as a comparison and flagged seven display panels. - a comparison against
0, becausesbtc > 0asks 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 asfeeLegson ajoinoruse-bombgame 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 allThe 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.
foldToAsciirewrites 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,buildPublishOpenLetterTransactionandbuildPublishFeedbackTransactionall 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: "var(--rt-touch-floor, auto)".
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('NONE' | 'ARWEAVE') (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:
UserProfileAggregatedescribesapps[].totals(newUserAppScopedTotals), the per-app slice that has been on the wire unnamed. Read it, not the protocol-widetotals, from anything wearing one app's brand.UserProfileAggregateSchemagained it plus fourtotalsfields it had been silently stripping.ProductTypeListParamsisRecord<string, never>, andStyleListParamsno longer extendsListQueryParams. Both used to type a query key their route rejects, solist({ 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
smit shows the first sentence of its message;TestnetBannertakes an optionalshortMessageto override that split. The paused (idle-sleep) message is never shortened. ConnectWalletButtonno longer ships afontWeight: 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 pendingThree 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
preferreturns exactly what it always did, so every grid in the estate is unaffected. - An edition carries its own card. Pass
product_instancefor 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.
useProtocolUserthrows 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, andtsccannot 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="STX" 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 standaloneApproved-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 noStyleSetProductTyperow, 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.
| where | what was small |
|---|---|
theme.css .rt-label | the 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 FootBar | sync state, block heights, the SHA, the version pins |
shell SubNavItem | the count beside a nav label |
shell RailAppStore | the 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.
conformance 0.6.0 (2026-08-24) - the registry is the routing table, and a launch is not a link
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 answerBoth 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
subjectandamountSbtcdescribe one moment. amountSbtcis 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 admitsreadability 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 ownsdk 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 <contract-id>::<that constant> - 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<string, string | number> 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 distinctownerAddresson the creator'sProductInstancerows, joined withUserwhen 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/connectandusers.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-profileProfileRail.
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) andErrorLog.status/ErrorLog.resolvedAtfields - error logs are now resolvable through the operator UITemplatePreviewInnerResponsecorrects theTemplatePreviewApiResponsepayload shape. The legacygenerated.svgfield never returned anything;preview.svgis the real path. BothstyleSets.generatePreview()andticketStyles.generatePreview()/generateLivePreview()return this shape consistentlySmartProductBase.productStickers- typed embed for?include=stickersqueries (was previously read viaas unknown ascast in consumers)ListProtocolEventsParams,ProtocolEventWithRelations- admin-only protocol-event log queryListStorageBlobsParams,StorageStats,StorageProviderName- admin-only storage subsystem visibilityPipelineProductTypeEntry,PipelineCompositionResponse- admin-only template-pipeline composition view
New admin services and methods:
admin.protocolEvents.list()- paginated query over the canonicalProtocolEventlog (filter byeventType,entityType,entityId,actorAddress,since,until)admin.storage.listBlobs()andadmin.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 typeadmin.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 calladmin.errorLogs.updateStatus(id, status)- flip an error log betweenOPEN/INVESTIGATING/RESOLVED;resolvedAtstamps automatically
SDK fixes:
apps.listApps()no longer double-wrapped on the wire (/api/appscollapsedsuccessResponse({ data: apps })tosuccessResponse(apps)); the SDK return type is nowApiResponse<App[]>as advertisederrorLogs.list()acceptsstatusfilter
rt-admin (consumes the above):
- New
/system/protocol-eventsand/system/storagepages - 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 ascasts eliminated by aligning SDK types to actual on-wire shape
Server-side schema:
ErrorLoggainedstatus(defaultOPEN) andresolvedAt(nullable) columnsappsroute 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 replacesActivityEventBlobStatus,UploadStatus,StorageRole,StorageBlob,StorageReference- storage subsystem split into three purpose-built tablesCollectionRules- consolidates the five sparse rule columns (fixedRecipient,fixedPriceStx,fixedPriceSbtc,maxItems,allowedEntityTypes) intoCollection.rules: Json
Removed (BREAKING):
DailyStatsEntryinterface andStatsService.getDailyStats()- the underlyingDailyStats/HourlyStatstables were never populated; the route never existed in rt-apiTag.usageCountfield - sort by attachedSmartProductTagcount via Prisma_countinstead.TagListParams.sortBynarrowed from'name' | 'usageCount' | 'createdAt'to'name' | 'createdAt'Collection.fixedRecipient,fixedPriceStx,fixedPriceSbtc,maxItems,allowedEntityTypes- read and write throughCollection.rulesnowStorageLocationinterface renamed toStorageLocationLegacy(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.totalPagestoplatform.totalProductsplatform.totalEditionstoplatform.totalInstancescontent.publishedPagestocontent.publishedProductscontent.burnedPagestocontent.burnedProducts
Server-side:
ApiKey.key(plaintext) replaced withApiKey.keyHash(SHA-256 ofpepper || 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
ProtocolEventrow 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 onSmartProductandProductInstanceall 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):
RevenueSplitinterface dropped from@rising-tide/sdk(src/types/index.ts). The@deprecatedJSDoc 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[]tocollaborators: CollaboratorInput[]+fundingGroupPicks: FundingGroupAllocation[]. NewContributionPickercomponent with a working FundingGroup allocator UI (closes inventory item #9). LegacyRevenueSplitDialog/RevenueSplitPieChart/RevenueSplitLegendand the 10-filerevenue-split/component dir deleted. - grammata write flow: form state migrated to substrate shape.
mapFeeDistributionsToContributionsbridge 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=splitstoken on/api/eventslisting dropped (zero consumers, no service implementation). Deadshared/splits.tspayment-creation util dropped (zero consumers).parseContributionPickis now the only revenue-split body parser.
SDK 0.6.11 (2026-05-05)
Server-side sort by currentInstances (Batch I).
/api/smart-productsand/api/bracketslisting routes now acceptsortBy=currentInstances. Powers cross-page "Most Signed" / "Most Collected" / "Most Entries" feeds without requiring client-side post-fetch reranking.SmartProductListParams.sortBySDK union extended to include'currentInstances'.BracketListParamsinherits.- 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
productTagsfrom?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
EventTagSDK interface andEvent.tags?: EventTag[]field. Replaced by the polymorphicproductTagsshape on the?include=tagspayload (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
SmartProductTagPrisma join (composite-unique on(productId, tagId), cascade both directions). MirrorsProductSticker/ProductContributionshape; works uniformly across PAGE / EVENT / PREDICTION. - POST
/api/smart-products,/api/events,/api/bracketsre-acceptbody.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=tagsnow works on detail and listing routes; SDKSmartProductDetailIncludeandSmartProductListIncludeunions extended.- New SDK type
ProductTagandproductTags?field onSmartProductBase. /api/tags,/api/tags/popular,/api/tags/[id]registry routes restored (autocomplete, popular suggester, single-fetch).- SDK
TagsServicetrimmed to read-only (listTags,getPopularTags,getTagById);useCreateTagReact 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
PageTagSchemaandTagSchemaexports from@rising-tide/sdk/schemas. Their only internal user (thetagsfield onPageSchema) was removed in 0.6.7; audit confirmed zero external imports. - Dropped dead
sdk.tags.createTagblock from rt-admin's pages and events create hooks. After 0.6.7 removed thetags: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 addSmartProductTagpersistence). - Fixed 2 known-flaky
http-client.test.tsresponse-interceptor tests. Root cause was async/sync mismatch: the http-client'sonRejectedis async, so throwing inside it returns a rejected Promise; the test's synchronoustry/catchnever caught it. Rewrote withPromise.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 servicecreateEventmethod body tagsfield inPageSchemaandEventSchemaZod schemas- POST
/api/smart-productsand POST/api/eventsvalidation 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. Excludesinstances(per-row blow-up) andbracketDetails/pageDetails/contributions/appListings(domain-specific or O(n) on listing). - Extended: detail-route allowlist now 8 keys (added
styleSetandstickers). 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:
SmartProductListIncludeandSmartProductDetailInclude.SmartProductListParams.include?: SmartProductListInclude[]. The SDK CSV-stringifiesincludebeforebuildQueryParams(the helper serializes arrays as repeated keys; the route parses CSV). - grammata
PageDetailre-injects styleSet artist + sticker info via the new detail include keys. TheuseStyleSets()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), addedpageDetails+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.pageStylesalias - usertClient.styleSetspageTemplateSetIdSDK param (was alias forstyleSetId)- Type aliases:
PageStyleSet,PageStyle,TicketStyleSet,TicketStyle,TicketStyleSetListParams Event.ticketStyleSetandEvent.ticketStyleSetIdfields- V1
useChainhookshook startBlockfield from V2 chainhook paramspageTypeIddeprecated alias onStyleListParams
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]/paymentsroute). SDK method renamedgetPagePayments->getProductPayments. Also dropped deadgetEventPayments- the substrate method covers all domains. 4 consumer call sites migrated.GET /api/smart-products/[id]/collections(replacesgetCollectionsForPage). 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:
getCollectionsForEventSDK method dropped (unused).pageDetailsService+page-details-service.tsdeleted (zero callers).- 3 obsolete
as unknown ascasts dropped (grammatauseCollectEdition, OTRuseSignLetter, bracket-blitzprofile/page.tsx). useBurnInstanceorchestration 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 bySmartProductPagefrom@rising-tide/sdkEdition- replaced byProductInstancePageType- replaced byProductTypePageAppListing,PageSticker,PageTag,PageListParams- dropped (substrate listing rows don't embed those relations)PageStatus- now an alias forProductStatus(same union plusLOCKED/SETTLED)
Removed services (BREAKING):
rtClient.pages- all five methods migrated:pages.listPages->smartProducts.list({ domain: 'PAGE' })pages.getPageById->smartProducts.getByIdpages.createPage->smartProducts.createPagepages.updatePage->smartProducts.updatePagepages.deletePage->smartProducts.deletePage
rtAdmin.pagesremoved for the same reason
Field rename map (consumer migration cookbook):
| Legacy field | Substrate field |
|---|---|
page.author (string) | page.ownerAddress |
page.authorName (string?) | resolve via user lookup |
page.editionsPublished / page.currentEditions | page.currentInstances |
page.maxEditions | page.maxInstances (now number | null) |
page.content (JsonObject) | (page.productConfig as Record<string, unknown>).content |
page.pageTypeId | page.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.pageStyleSetId | page.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.pageId | instance.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/styleSetsresolution 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
currentInstancessubstrate 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:
productTypeSlugfilter (resolves to productTypeId server-side via the cached ProductType registry; unknown slugs return empty list)collectionIdfilter (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 tortClient.productInstancesandrtClient.users.getUserInstances.rtClient.pageTypes(PageTypesService class) - 4 of 5 methods hit dead routes; the survivinglistPageTypeswas sugar over substrate. Migrate tortClient.productTypes.
Trimmed PagesService:
- Dropped:
getPageEditions,getListedApps,listOnApps,removeAppListing,getTournamentLeaderboard,getPageContributions, plus the deprecated aliasespublishToApps,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/pagessugar routes; full removal queued for 0.6.0).
Removed React hooks:
useEditions.tsfile (entirely dead, no exports were live)usePageTypes.tsfile (onlyusePageTypewas 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 EditionsServicePageTypeListParams- only used by dropped PageTypesServiceWritingProductType(alias forPageProductType)WritingContractProcessRequest(alias forPageContractProcessRequest)
Removed rt-api routes:
/api/page-types(GET sugar) - no callers remain after consumer migration toproductTypes.list({ domain: 'PAGE' })
Consumer migrations:
- All 7
editions.listEditionscall sites migrated tousers.getUserInstances(rt-profile x5, OTR, grammata) - rt-admin
PageTypeDialogrefactored: form now takes substrate fields (slug + displayName + label) with auto-derived slug on create - rt-admin
settings/page.tsxpage-types tab fetches viaproductTypes.listfiltered to PAGE domain - grammata
useCollectionFiltersmigrated fromEditionListParamstoUserInstancesParams(dropssearch,minPrice,maxPricefilter fields - substrate doesn't support them server-side) - grammata
usePageTypesrewritten to callproductTypes.listand 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 legacyEditionshape; deferred to 0.6.0 with the full Page/Edition UI type refactor
Dropped service exports from RisingTideAdmin:
rtAdmin.editions,rtAdmin.pageTypesremoved 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.deployTxIdfield added
Deprecated (still functional, slated for removal):
rtClient.pages- usertClient.smartProducts(per-method substrate map in PagesService JSDoc)rtClient.editions- usertClient.productInstancesandrtClient.users.getUserInstances(per-method map in EditionsService JSDoc)rtClient.pageTypes- usertClient.productTypes(per-method map in PageTypesService JSDoc)rtClient.pageStyles- usertClient.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+/listtriple)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/lookuppageId 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- usertClient.smartProducts(per-method substrate map in PagesService JSDoc)rtClient.editions- usertClient.productInstancesandrtClient.users.getUserInstances(per-method substrate map in EditionsService JSDoc)rtClient.pageTypes- usertClient.productTypes(per-method substrate map in PageTypesService JSDoc)rtClient.pageStyles- usertClient.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 thertClient.smartProductsmethods directly inside your own mutation wrapper -createPage/updatePage/deletePage, their event and prediction equivalents, andpublishPrediction- plusrtClient.productInstances.burn.
Still exported (legacy services kept):
rtClient.pages,rtClient.editions,rtClient.pageTypes,rtClient.pageStylesservices remain alongsidertClient.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 paginatedProductInstancerows 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 routeGET /api/users/address/{stxAddress}/instances - Filters:
domain(PAGE | EVENT | PREDICTION),status,parentProductId,sortBy,sortOrder,page,limit - New input type
UserInstancesParamsexported 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 (usertClient.privy.signBurnEditionor 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(composesAuthMethodProvider+AppModeProvider+ModeGate+ConditionalProviders+ProtocolUserProvider), genericAuthGateroute guard withallowlist/customMatcher/requiredEntitlementprops,useEntitlement(verb)shorthand - New
RisingTideQueryProviderre-export (alias ofRisingTideProvider) rtClient.instancesandrtClient.styleSetsnamespace aliases (resolve toproductInstances/pageStyles)nextadded as optional peer dep (^15) fornext/navigationconsumed by SDKAuthGate- rt-profile migrated to SDK
AuthGate; localAuthGate.tsxreduced to aroute-matcherhelper
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.appIdenforcement inevaluateTemplateScope- FundingGroup routes hardened (Zod + transition guards)
- PATCH escape hatch closed (
rtClient.users.updateAppPreferences) - Mainnet seed UUIDs runtime-generated with
ALLOW_MAINNET_SEEDguard - 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:
Inviteentity with 5-state lifecycle - 60-bit Crockford Base32 codes with CSPRNG
- Role-on-claim support
- Mint quotas by role