Rising Tide Developers
Concepts

Achievements

The progression ladder - awards derived from the event stream that open apps and mint invites, and the two read surfaces an app builds on.

The protocol grants achievements for what an address has already done. They are derived, not claimed: nothing a client sends earns one, and there is no endpoint to request one.

Two families:

  • ladder - the onramp. Seven rungs, climbed in order: each names the rung before it, and it cannot be earned until that one is held. Each rung is a create, a collect or a won match, and its payload is a door or an invite. It walks a new account through the five apps - a bottle, a reply, three bottles, two letters, a page, a match, a pot - and ends with an invite to give.
  • return - usage. Distinct days on which the address acted, protocol-wide. Earned by coming back rather than by advancing.

How an award is earned

A projector maintains counters off the canonical event stream. An award is written when every requirement on a definition holds - the named counter, for that address, at or above its minimum.

ProtocolEvent
PRODUCT_PUBLISHED, INSTANCE_MINTED, INSTANCE_SCORED
Counter
per address, per app
Award row
written by the projector
Settle
the payload runs
Announce
ACHIEVEMENT_UNLOCKED

Writing the row, running its payload and announcing it are three separate steps reading three separate timestamps, so a redelivered event cannot double-pay. Settlement and announcement run on the projections cron, after the sweep.

Four counter kinds exist: events an address was the actor of (scoped to an event type and an app), collects of somebody else's instances, matches played (every seat on an INSTANCE_SCORED, whatever the result), matches won (an INSTANCE_SCORED that names the address as the winner - a draw counts for nobody), and distinct active days protocol-wide. A counter is kept per app; a rung walked in more than one app reads their counts summed, and its requirement names every app that counts. A day counts only if the address published, collected or won on it - a sign-in, an app link or a profile edit earns nothing, so copy that implies otherwise promises a rung that never lands.

A ladder rung has one requirement beyond its counter: the rung before it must already be held. A counter can run past its threshold early - two open letters written before the third bottle - and the rung waits. When the rung before it lands, everything it unblocks is written in the same transaction, on the same event and with the same earnedAt, so replaying the stream in any order produces the same rows. The return family has no order; a day is a day.

What a rung pays

Two payload kinds, and deliberately no third:

PayloadEffect
unlockAppAn AppAccessGrant with source ACHIEVEMENT, so the app opens for that user
grantInviteAn invite minted to the user as holder, under the drip's ceiling

There is no token payload. Gas comes from the invite that funded the account, and a rung that paid currency would make the ladder a faucet rather than an onramp.

grantInvite may carry renewsEvery. The fifth-day award does: an invite on the fifth distinct active day, and one more for every ten further days. The invites ledger is what says how many have been paid - the count of INVITE_ASSIGNED events whose payload names the award and the holder - so a rung that renews is idempotent against a replay without needing a column of its own.

The two read surfaces

An app never computes any of this. It reads one of two shapes.

The shelf is per address:

const shelf = await rtClient.users.getAchievements(stxAddress, { appId });
// shelf.awards - earned, newest first, each with its line resolved and the app it happened in
// shelf.next  - exactly ONE next rung, or the graduated state

next is present only when the bearer token is that address. A missing token is a stranger and gets awards with next: null; a failing token is a 401. appId says which app the viewer is standing in, which changes how the next step is worded, never what it is.

The ladder is the same for everybody:

const ladder = await rtClient.users.getLadder();
// ladder.rungs   - the onramp, in the order it is climbed
// ladder.returns - earned by coming back

It takes no address and no bearer token, which makes it the one achievements shape a signed-out visitor can read. Join it with a shelf on slug to mark where a viewer stands.

Every LadderRung carries precursor: the slug of the rung before it, null on the first rung and on every return rung. A rung whose precursor is not on the shelf is locked, and the field says what it waits on:

const held = new Set(shelf.awards.map((a) => a.slug));
const locked = ladder.rungs.filter((r) => r.precursor !== null && !held.has(r.precursor));
// each locked rung: "after " + ladder.rungs.find((r) => r.slug === rung.precursor)?.name

The two are companions rather than alternatives. The shelf deliberately returns one rung: a person climbing should see the step under their foot, not the whole staircase. The ladder is for the page that shows the staircase.

Rendering the next step

useNextRung(sdk, { appId }) from @rising-tide/ui/react is the hook behind the one-line guidance surface. The rule the estate follows:

The next rung is one protocol fact. An app decides only where the line sits and which of its own words to say it in - never what the fact is.

An app may substitute a word its lexicon bans (a reply for a note, a lowercase product name). It may not substitute the requirement, the count, or the app the rung points at.