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.
action | Contract function | Who may sign |
|---|---|---|
lock / unlock | lock-tournament / unlock-tournament | the owner |
enter-results | enter-results-batch | the owner |
clear-result | clear-result | the owner |
play-in-result / clear-play-in | enter-play-in-result / clear-play-in-result | the owner |
finalize-scores | finalize-scores-batch (up to 50 entries) | anyone signed in |
claim-prize | claim-prize | anyone signed in; only the leading entry's owner succeeds |
withdraw-pool | withdraw-prize-pool | the 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); // BracketRoundTallystatus | Meaning |
|---|---|
not-open | The window has not opened, or the round before is not final, so the matchups are not known |
provisional | The count can still move: the window is open, or closed less than an hour ago |
counting | The hour has passed and the final count is not stored yet |
final | Stored 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.