budget.
Program: MinXAXaPZezFMc8mm1uZnK8NcYt7Fhbr7Eifdnw7cfH · Config PDA:
3WQ7QsWJJhqa1REGjUJFzCUNPSonbaXKcZoJbDwQnYq4
Decode with the SDK. Do not transcribe the layouts.
The SDK decodes Minute’s accounts and events, so no layout needs to be copied into your repository:decodeMinuteEvent and the event decoders next to it rather than deriving your
own.
Read the governed dials from the chain. Every time.
min_stake, max_stake, entry_fee_bps, exit_fee_bps and initial_timer_s are governed.
They move by a transaction, with no release on your side and no warning.
Read them with decodeMinuteConfig(bytes) from the account at minuteConfigPda(), and prefer
failing closed when the config is unreadable over falling back to a remembered value.
The fee model: fee-inclusive, top to bottom
Both layers take their fee from INSIDE the number handed to them, and neither adds anything on top. The number the player types is the number that leaves the wallet. The two money instructions:total_in / added_total_in is the fee-inclusive total — the exact number that leaves the
wallet, and what min_stake / max_stake bound. min_collateral_out /
min_added_collateral_out is a floor on the resolved position: forwarded to the venue and
re-checked by Minute against what the venue actually minted, so a venue fee that moves between your
quote and execution reverts instead of quietly buying less. 0 is refused by name
(MinCollateralOutRequired: 6050 in Minute’s table, 6168 in the venue’s) — it is the absence of a
floor, not a floor. The venue’s own 6050 (RungAboveMarketCap) is reserved and raised by nothing.
An add is always exactly the round’s opening total: added_total_in == round.opening_gross, which
is also the open’s RoundOpened.gross_stake. One field, read off the round or off the event — not a
range, and not round.stake or round.opening_stake, both of which hold the venue-resolved
collateral and are refused by name (AddMustEqualOpeningStake, 6042 — Minute’s code; the venue’s 6042 is reserved and never raised).
1
Minute's fee is ABSORBED
entry_fee_bps comes out of the stake, not on top. A player who commits $1.00 has exactly
$1.00 leave their wallet toward Minute, of which the fee is a part. Minute never adds to what
the player typed. min_stake and max_stake bound this gross figure, so a typed $0.25 is
a legal 250,000 stake — do not gross down for Minute’s fee.2
The venue is ALSO fee-inclusive: Minute is a pass-through
The venue takes its own fee from inside the budget it is handed, exactly as Minute does. Minute
takes its cut off the top and hands the remainder DOWN to the venue as a
budget; the venue
takes its own entry_fee_bps from INSIDE that budget and opens the largest position the
remainder buys. Nothing is added on top at either layer, and a round’s timer costs nothing
extra. So the wallet debit is the number the player typed: one SPL transfer, of exactly
total_in, and both fees are paid out of it.The wallet debit is a single transfer of the typed amount. The fees round the POSITION, which is
why
min_collateral_out exists.The venue’s entry fee is 0, so the venue takes nothing from the budget it is handed.Minute’s own entry fee and exit fee are 0 too. A round’s close pays the venue’s exit fee on the terms of a
Knockout position on that market: see Fees and limits. Read both programs’ fees from
their config accounts and the market’s fee-terms account.
min_collateral on the RESOLVED position, not
a grossed-up stake: read it from the chain, and set the CPI’s min_collateral_out to the collateral
you predicted for the budget — never a token value like 1, which satisfies the check and protects
nothing. If you need a specific minimum position, that is a governance question about min_stake and
min_collateral, re-derived whenever the venue moves its own fee, which Minute does not control.
The ceiling is not max_stake
max_stake bounds the opening stake. A double-down adds to a live round, so total exposure runs
past it.
max_adds = 0 means uncapped, not “no adds”. The binding constraint is the venue’s maximum
collateral per position, config.params.maxCollateral on the venue’s config, and the refusal is
Minute’s AddExceedsVenueCapacity (6046). So the ceiling is the venue’s number and the error is
Minute’s; do not look for this code in the venue’s table.
Markets: the venue’s per-market gate
Minute keeps no market list of its own:Round.market may point at any market the venue has
listed, and an unlisted asset has no Market PDA to pass. The venue then applies one gate of its
own: a Minute player’s book may open or add only on a market whose minuteEnabled flag is true.
Anywhere else the venue refuses with MinuteDisabled (6201). Settles are never gated, so a live round
is never trapped by it. Offer a round only on a market whose minuteEnabled is true and whose paused is false (both on decodeMarket(bytes)). Only the venue’s config authority changes either. Minute is enabled on BTC, ETH, SOL and XRP.
Markets with trading hours
No live market has trading hours. Gold (XAU/USD), which is not available for now, trades in sessions (markets). The calendar is on the market (sessions, sessionCount, sessionBufferS on decodeMarket(bytes)); read it there, not from
this page. On such a market the venue’s rules apply to a round exactly as to any position:
- An open is refused outside a session (
MarketClosed, 6094) and near its close: once less thansessionBufferSplus 300 seconds of the session remains (SessionEndsBeforeTenor, 6095). The venue shortens the round’s position so that it endssessionBufferSbefore the close. - A double-down is refused outside a session (
MarketClosed); inside one the venue admits it, because the position already ends before the close. Minute refuses one that would carry the clock past the shortened position (ExtensionExceedsTenor, 6023, Minute’s code). - A price signed at or after the close cannot settle the round (
QuoteOutsideSession, 6295, the venue’s code). A round that is still open at the close is closed by the venue’s no-price close,lastResort, which returns the stake at par once the position’s tenor plus the venue’s last-resort grace has passed: about a day later. Minute’s keeper sends it, andreconcile_roundthen records the round. A round is not left open at the close in the ordinary course: its clock ends well before it.
sessionBufferS plus 300 seconds before each close until the session
reopens, and show the player when the market reopens.
Who pays, and who may sponsor
Three roles, and they are separate on purpose:open_round and double_down are sponsorable. Your backend can be payer, and a delegate the
player approved can be funder, so the player never signs the open and never needs SOL.
init_player is the exception: it requires payer == player_wallet, and refuses anything else
with PayerMustBeThePlayer (6038).
The shape that works: put a lamport transfer to the player and
init_player in one
transaction, with the player as payer and signer. You carry the cost and the player never sees
a fund-your-wallet step. It happens once per player.close_player.
Settlement is permissionless, and a redundant settle is not a no-op
Anyone may settle a round’s venue position once its clock has expired. If you run your own closer alongside Minute’s keeper, that is fine and encouraged — but know the shape of the race: A second settle of an already-settled position FAILS with the venue’sPositionNotFound (6057).
It is an error, not a silent success. There is no double-payout and there cannot be, because the
state transition gates the money rather than following it — the loser of the race gets a reverted
transaction, not a second payout.
Error codes are meaningless without their program
Anchor numbers every program’s errors from 6000 — so even 6000 itself collides (Minute’sNotAuthority vs the venue’s RateOutOfRange). So:
6027is Minute’sRoundNotLiveand the venue’sReserveUnderflow6007is Minute’sWrongMintand the venue’sNoFreshPrice
Opening, and the venue’s price
open_round and double_down reach the venue’s open and add_collateral by CPI. Those read no
price, so neither Minute instruction needs a quote instruction in front of it. The round’s venue
position is an order until a filler writes its entry at the venue’s next price after a short
delay, and a double-down is priced the same way or returned. A double-down the pool has no room to
cover is returned in full, and the round’s total stays within the venue’s maximum position. Read the
entry from the venue’s events, not from Minute’s: see events of an order.
Minute forwards its remaining accounts to the venue. The SDK’s minuteIx.openRound and
minuteIx.doubleDown take them as their last argument and refuse an empty list:
market.feeds: feeds[0] is a feed id, not an account.
Closing a round is the venue’s settle on the player’s book, followed by reconcile_round.
settle_round is refused while the venue runs its liquidity pool (CapitalRequired, 6265, the
venue’s code), because the pool settles every position directly so that part of a win can be queued.
A knockout, take-profit or expiry close carries the quote’s ed25519 instruction immediately before the
venue’s settle, and priceAccountsFor(market, config, 'settle') gives the whole list of remaining
accounts. An expiry takes two transactions: anyone marks the venue position’s expiry (mark_expiry)
from the bell, and the settle follows from the mark’s exitTarget on, with the window quote. See
settling a position. Anyone may send the settle, and the
venue’s keepers do. reconcile_round then records the round on Minute’s side. If no price can be
obtained at all, the venue’s no-price close, lastResort, applies to the round’s venue position
directly (see settling a position), and reconcile_round then
records the round.
Do not gate a close on a pause. Market.paused refuses opens and adds (and a take-profit stamp
or retarget), never settle. Pause your OPEN button if you must; never your close.
Events of an order
Minute’s events areRoundOpened, RoundExtended, RoundSettled, RoundReconciled, Swept,
PlayerInitialized, PlayerResized, PlayerClosed, ConfigUpdated, PauseChanged and
LookupTableSet. It has no event named DoubledDown.
Minute’s two round events are emitted when the order or the add lands, before the venue has priced
it:
RoundOpened.entry_priceis 0 on every open.RoundExtendedis emitted at the add’s commit, also for a double-down that is later returned. Itsentry_priceequals itsentry_price_before.opened_slotin both events is the slot of the commit, not the slot of the entry price. The holds count from the venue’sEntryFilled.quote_slot, and after a filled double-down from the venue position’sopened_sloton the book.
EntryFilled.entry_price. The entry after a double-down is the venue’s
CollateralAdded.entry_price, with entry_price_before. A returned double-down is the venue’s
AddReversed. Join them to the round on opener_tag.
The transaction is tight
A wrapped open names the Minute accounts and the venue accounts it reaches, against Solana’s 1,232-byte packet limit. Compile a v0 message against two lookup tables. The first is the table Minute’s config names (decodeMinuteConfig(bytes).lookupTable); read it from the config rather than
from your own settings. The second is the venue table for the market, from chooseMarketTable
(see session keys). Both are frozen, so cache their contents by address. Measure the signed packet before you send it, and refuse one that does not
fit rather than letting the RPC reject it.