Rising Tide Developers
SDKCore

rtClient.brackets

SDK methods for prediction brackets - sugar over the PREDICTION domain, plus the entry and lifecycle surface a generic product has no equivalent of.

A bracket is a smart product in the PREDICTION domain, so rtClient.smartProducts can read one. rtClient.brackets exists for the parts that have no generic equivalent: entries, a leaderboard, and a lifecycle with more states than publish.

Reads return SmartProductPrediction rather than SmartProduct - the domain narrowing is enforced by the route, so you do not cast.

Reading

const res = await rtClient.brackets.list(params);        // PaginatedResponse<SmartProductPrediction>
const { data } = await rtClient.brackets.getById(id);

getEntries(bracketId, { ownerAddress?, page?, limit? }) returns PaginatedResponse<ProductInstance> - an entry is an edition, the same primitive a collected page is, which is why prize distribution rides the same revenue splits. getEntry(bracketId, entryId) fetches one. getLeaderboard(bracketId, limit?) returns BracketLeaderboardEntry[].

getContributions(id) returns the frozen Contribution[] - who gets paid, in the positions baked into the contract at deploy.

Authoring

createDraft(input) takes a CreateBracketDraftRequest and returns the product with its bracketDetails. updateDraft(id, patch) and deleteDraft(id) work on it while it is still a draft.

Lifecycle

Beyond publish(id), everything that happens to a tournament happens on chain, so it is a command rather than a method here: rtClient.commands.submit({ kind: 'bracket-action', ... }). The database follows what the contract did through the chain handlers - there is no route that locks, settles or names a champion on its own.

actionContract functionWho may sign
lock / unlocklock-tournament / unlock-tournamentthe owner
enter-resultsenter-results-batchthe owner
clear-resultclear-resultthe owner
play-in-result / clear-play-inenter-play-in-result / clear-play-in-resultthe owner
finalize-scoresfinalize-scores-batch (up to 50 entries)anyone signed in
claim-prizeclaim-prizeanyone signed in; only the leading entry's owner succeeds
withdraw-poolwithdraw-prize-poolthe owner, once no entrant can claim

"The owner" is the address that deployed the contract. Nothing signs in allow mode: a claim and a withdrawal carry deny-mode ceilings on both pools, and every other action moves nothing. On the wallet path pass pool: { stx, sbtc } for those two, read from get-tournament-state; the Privy path reads the pool itself.

The lock is the point of the whole shape: it is what makes a prediction provable rather than claimed. The claim needs every live entry scored, and the owner can take an unclaimed pool back only after the claim window (1008 burn blocks) - so the pool reaches the best bracket or, failing a claim, its owner, and never the owner first.

An entry is paid in STX (submit-bracket) or sBTC (submit-bracket-sbtc). A currency priced at 0 is not offered while the other is priced - the contract refuses the entry with u2018 rather than let it join a paid pool for nothing. A tournament priced at 0 in both is free to enter.

A tournament settled by an outside count names a results oracle at deploy: one principal that may lock the tournament and post results, and nothing else. The owner may post such a tournament's results only when they equal its final tally.

A tournament settled by a count

A song battle is a bracket whose results are counted rather than reported: each song's paid plays on xtrata inside a round's window, read from mainnet. Its productConfig names the oracle, a seed and a subject ({ kind: 'xtrata-song', core, id }) for every team, one rounds entry per bracket round with startsAt and endsAt, back to back, and tieBreak: 'higher-seed'. Round 1's matchups are the config's games, and every later round is the winners of the one before. POST /api/brackets checks that round 1 starts in the future and that every song resolves on the oracle, and refuses the tournament naming the song that does not.

const { data } = await rtClient.brackets.getRoundTally(bracketId, round); // BracketRoundTally
statusMeaning
not-openThe window has not opened, or the round before is not final, so the matchups are not known
provisionalThe count can still move: the window is open, or closed less than an hour ago
countingThe hour has passed and the final count is not stored yet
finalStored once and never recomputed, with every counted play in countedTxIds

Each row names both songs with their plays, the winner, and decidedBy: plays, or seed when the plays were level and the higher seed went through. finalTotal is the last round's combined plays, the number entrants guessed as their tiebreaker. A live count the source cannot answer is a 503 with code COUNT_UNREACHABLE.

The protocol wallet is the results oracle. It locks the tournament when round 1 opens and posts each round from its stored final tally, through the same outbox as every protocol-wallet write; if the owner's fallback posted first, it finds the round decided and sends nothing.


Bracket Blitz, the app built on this service, is not open for anyone to host a tournament yet. The service is live and the contracts are tested; the organizer surface is by request.