Skip to content

Add predict (prediction market) + copy-trade example apps for Flash V2#1

Merged
Kaleb-Rupe merged 26 commits into
mainfrom
feat/predict-updown
Jun 15, 2026
Merged

Add predict (prediction market) + copy-trade example apps for Flash V2#1
Kaleb-Rupe merged 26 commits into
mainfrom
feat/predict-updown

Conversation

@Kaleb-Rupe

Copy link
Copy Markdown
Collaborator

Adds two production-grade example apps for Flash Trade V2 (Solana perps on a MagicBlock Ephemeral Rollup, mainnet), both reusing the proven tap-trade enable / session-key / signer wiring.

examples/predict — a prediction market on capped-loss perps

A "buy YES" is a single capped-loss Flash V2 perp + a bundled take-profit (the win), so you can never lose more than your stake.

  • Spread-aware odds engine — the trade spread is paid on both legs (entry and exit); the strike is grossed up by 1/(1−s). Live-verified: shown to-win matches the venue's real takeProfitQuote within ~3% across SOL+BTC headline + full ladder.
  • Two-step commit — reconciles every bet to the signed fill and blocks the sign if the live take-profit pays ≤ 0 or diverges from the shown odds (never trust local math over the venue's number).
  • Settlement engine — records each bet, auto-closes at its deadline, resolves win/lose; a guard prevents auto-closing a position blended with another app's.
  • Any-wallet connect (Wallet Standard ref-dance, matches tap-trade), standalone deposit/withdraw, root route /.
  • Went through a 3-agent adversarial review + a verifier pass; every CRITICAL/HIGH fixed. The mainnet "real funds" disclosure stays up until the documented F7 guard + scoring residuals are signed off — see SPEC-PREDICT-V2.md → v2.1 REVIEW OUTCOME.

examples/copy-trade — mirror a winning trader

Ranked leaderboard (public fstats API, server-side) → pick a leader → mirror opens/closes with proportional sizing, manual-confirm or capped auto-copy, session-key signing, budget + kill switch.

Notes

  • Mainnet, real funds — both apps disclose this in-app.
  • No secrets: the RPC key lives only in gitignored .env.local (cp .env.example .env.local); the repo's CI secret-scan covers this PR.

🤖 Generated with Claude Code

Kaleb-Rupe and others added 26 commits June 12, 2026 08:54
…engine

Convert examples/copy-trade from a CLI into a full Next.js app, reusing
tap-trade's account infra (flash/session/signer/enable/funds/hooks/format).
New: lib/leaders.ts (fstats V2/ER leaderboard via a same-origin /api/leaders
route — fstats has no CORS), lib/copy-engine.ts (port the diff → collateral-
ratio sizing → replay loop into a React hook with manual-confirm + capped
auto-copy + budget + $11-floor skip + kill), and a Liquid Glass design
system (OLED surface, mesh glow, frosted double-bezel panels). app.tsx wires
wallet → enable → session signer → leaderboard → follow a leader → live book
→ mirror. Typechecks + builds clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d filters

- Engine baselines the leader's current book on follow (copy their NEXT move,
  not a position they're already mid-way through); plain skip reasons
- Console rewritten in stupid-short language: 'How to copy' (Ask me / Auto),
  'your balance' tappable -> deposit/withdraw, 'Total budget' / 'Max per trade'
  with hints, 'Used $X of $Y'; activity + your-copies headers
- Wired full V2 deposit/withdraw FundsSheet (two-step withdraw) into the app
- Leaderboard sort (Top PnL / Win rate / Volume / Most trades) + in-a-trade filter
- Labeled position rows (size / lev / entry / pnl)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- parseAmount/fmtAmount in format.ts: tolerate US + EU number formats
  ('1,234.50', '1.234,50', '12,5'), spaces, leading '.', empty -> 0, never NaN
- NumField rewritten with local text state: clearable, type decimals freely,
  comma or dot, clamp to a floor only on blur (drop the Math.max that fought
  every keystroke); same comma support on the deposit/withdraw fields
- Engine: split the misleading 'deposit USDC to copy' skip — only say that when
  the FOLLOWER has $0; when the LEADER's position has ~0 collateral (∞ leverage)
  say that instead, so a zero-collateral leader isn't blamed on your wallet

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Auto-copy now KEEPS RUNNING when you return to the dashboard (split nav 'view'
  from the bound 'leader'; leaving only stops it when not armed). One leader at a
  time — other leaders are locked while a session runs, with a resume banner.
- Global header indicator: '1 copying · leader · N open' (running) or 'N open'
  (idle with positions) + a Stop all / Close all button on every screen.
- Stop all = disarm auto AND market-close every open copied position; each close
  is independent (one failure can't abort the kill switch; leftovers stay visible).
- Leaderboard: single-pick Sort + STACKABLE multi-select Filters (In a trade,
  Profitable, >=10 trades, Win >=50%) with a clear button.
- Removed the now-redundant per-leader unfollow (Back + global Stop cover it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Reverted the extra filter row (Profitable/>=10/Win>=50). Back to the four
  metric chips (Top PnL / Win rate / Volume / Most trades), now MULTI-SELECT:
  pick several and they rank by a normalized blend (strong on all picked). The
  'In a trade' toggle is independent again. Single clean row; active = glass pill.
- Promoted Stop all out of the header chip to a BIG RED button on the right edge
  of the 'Copy a winner.' hero (shows when running or positions are open). Header
  keeps only the lightweight '1 copying / N open' indicator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d UP/DOWN, session-key settlement)

New examples/predict workspace: timed price-call rounds where each round is
a real session-signed perp — ownerless quote at review, signed response =
locked numbers, observed-state settlement engine with chain-truth
reconciliation and orphan adoption. Plain-words UI (zero trading jargon),
glassmorphism system, live price chart, searchable market picker, sheets
conditionally mounted (closed markup can't render unstyled). Reuses
tap-trade's proven session/enable/funds/balance/stream lib verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Transform 'Updown' (capped-loss UP/DOWN timer) into a real-feeling prediction
market on Flash V2 perps (Drift BET / Kalshi model: cents = probability, YES/NO,
to-win, win/lose settlement) — confirmed direction: strike thresholds +
multi-outcome buckets.

- lib/markets.ts (pure, typechecked): a YES/NO market = a capped-loss perp with a
  bundled take-profit at the strike (YES win) + the liquidation knockout (NO/lose).
  YES price q = 1/(1+R), R = L·(t−fee). strikeLadder() (85/70/55/40/25¢),
  bucketMarket() (normalized multi-outcome), openParamsFor() bridge, cents/to-win
  helpers. Honest: odds are formula-set, payout from LP, loss capped at stake.
- SPEC-PREDICT-V2.md: locks direction, the algo math, the UX (Kalshi/Polymarket-
  grade on the existing glass), design tokens from the premium research, phases.

Phase 1 of 6. typecheck green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- lib/use-markets.ts: live market rows from /v2/tokens + polled /v2/prices,
  categorized (Pyth-ticker prefix, hidden unless >=60% classify).
- components/market-card.tsx: THE card — token + live price + plain-words question
  + YES/NO odds as a split bar (cents = odds; semantic colour + up/down arrows,
  never colour-alone). Headline priced live by the engine (marketForTargetProb).
- components/discover.tsx: category tabs + search + timeframe + responsive card
  grid; wallet-free browsing; honest footer disclosure.
- app/markets/page.tsx: standalone /markets route to view it without touching the
  existing orchestrator.

typecheck + next build green (/markets generates). Phase 2-3 of 6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- components/market-detail.tsx: tap a card -> overlay with an Above/Below toggle
  (each side a real, separate bet -> odds don't sum to 100, the gap is the honest
  edge), the STRIKE LADDER (a row per strike at its own odds), MULTI-OUTCOME
  BUCKETS (normalized 'where does it land?'), and the BUY TICKET (stake -> to-win
  / profit / max-loss = stake, locked math,  floor). All from lib/markets.ts.
  Real on-chain execution = phase 5 (needs the wallet/session orchestrator); the
  ticket is an honest preview here.
- discover.tsx: card tap opens the detail overlay.

typecheck (all workspaces) + next build green (/markets 4.61 kB). Phase 4 of 6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- market-detail.tsx: the ticket now PLACES a real bet — openPosition with the
  bundled takeProfit at the strike (YES win) + the liquidation knockout (lose
  stake), via the same owner+tradeFields+sendTrade path app.tsx proves. Status
  (placing/done/error) + receipt; gated on a signer ('Connect & enable to bet').
- markets-app.tsx: /markets shell reusing the proven wiring (providers -> owner
  stream -> loadSession -> makeSessionSigner -> enableOneClickTrading). Minimal
  wallet bar: connect / enable one-tap / basket balance. canBet = basket + signer
  + >=$1 deposited.
- discover.tsx threads signer/canBet/onNeedWallet/onPlaced to the detail.

typecheck + build green (/markets 292 kB w/ wallet adapters). Phase 5 of 6.

NOT yet through the mandatory adversarial review (subagents rate-limited) — do not
trade real funds on it until reviewed. Remaining: portfolio of open bets + the
funds sheet on /markets (5b), then disclosure + polish (6).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- components/portfolio.tsx: 'your open bets' — each open Flash V2 position is a
  live bet (LONG = Above call, SHORT = Below), live value via computePositionView
  (Flash-parity mark math, GOTCHAS §20), marks from one polled bulk /v2/prices.
  READ-ONLY: no trade is placed here, zero funds risk.
- markets-app.tsx: renders the portfolio above the grid when connected.
- market-card.tsx: subtle hover lift (premium touch).

typecheck (all workspaces) + build green (/markets 6.58 kB).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The card was solving for a fixed 45c target (moving the strike to hit it), so
every card showed YES 45c / NO 55c. Now it fixes a round-number strike a
believable distance above the live price and lets the odds FLOAT from priceMarket
— SOL above $80 (+8%) prices longer than BTC above $70k (+4.7%), etc. Real,
per-market odds. Card-only display change; the money path is unchanged.

typecheck green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uction)

The 3-reviewer money-path audit found the v2 engine funds-loss-grade broken:
modeled cost as 0.16% fee, ignored the real 5-10% Flash spread -> bundled TP fired
as a ~100% loss on SOL (live pnlPercentage -100), leverage unclamped (100x already
liquidated), nothing reconciled to the signed fill.

Decision: keep the TRUE fixed-odds binary (Path A), corrected for spread:
- everything measured from fillEntry = oracle*(1±s), strike = fillEntry*(1±t),
  knockout = fillEntry*(1∓0.92/L); R = L*(t-fee), q = 1/(1+R) -> honest odds.
- new hard constraint: knockout must sit beyond the spread ->
  L_max = min(custody.maxLeverage, 0.92/(s*1.5)). SOL (s=0.10) => L<=~6x.
-  floor is correct (bundled TP needs >$10 after fees; bare-open is ~$5).
- full fix list (every CRITICAL/HIGH from the audit) + order in the spec.

Design locked; engine rewrite + wiring + reconcile + settlement port is the next
focused session, re-reviewed before the 'don't trade' banner comes off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
 #1,#2,#8,#9)

Engine (lib/markets.ts) rewritten to measure from the SIGNED FILL, not the
oracle — kills the funds-loss bug where a take-profit computed off the oracle
landed inside the 5-10% Flash spread and fired as a ~100% loss:
  fillEntry = oracle*(1±s); strike = fillEntry*(1±t); knockout = fillEntry*(1∓0.92/L)
  R = L*(t−fee); q = 1/(1+R)
Leverage clamped at the chokepoint priceMarket so koDist=0.92/L always clears
the spread (L ≤ 0.92/(s·1.5); SOL ⇒ ~6x, never 100x already-liquidated). Ladder
spans by STRIKE once leverage pins at the cap, so odds float per token (answers
'why are all the markets the same odds'). Degenerate inputs never NaN/throw (#9).
Slippage derived from the spread, never a hardcoded 1% (#8).

Wiring (#2): one shared custody+tokens parse path (deduped in-flight) feeds both
useMarketLimits (detail) and a new useAllMarketLimits (grid) — the fund-critical
spread is derived in exactly one place. Card + detail gate on live limits and
NEVER render spread-blind odds.

Pure engine verified (all asserts pass); full typecheck green. The money-path
reconcile (#3), trigger validation (#4), settlement (#5), gates (#6) and bucket
redesign (#7) are the remaining steps; 'don't trade real funds' banner stays up
until the 3-agent re-review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ixes #3,#4)

LIVE-PROBE FINDING (read-only mainnet quote): the spec's v2.1 math charged the
spread only on ENTRY. The venue charges it AGAIN on exit — a take-profit
triggering at K exits at K*(1∓s). A $90 SOL TP came back exitPriceUi=$81,
profitUsdUi≈$0: a bet shown as 59¢/'to-win $42' actually won ~$0. The
single-spread strike landed at break-even.

Engine (lib/markets.ts): strike now grossed up by 1/(1−s) so the NET entry-fill
→exit-fill move equals t; priceMarket derives t from exitFill (both legs).
headlineMarket targets a real prob via the corrected solver then rounds the
strike close (was overshooting 58¢→30¢). VERIFIED LIVE: across SOL+BTC headline
+ full ladder, engine shown-to-win matches the API takeProfitQuote within 3%.

Two-step commit (#3) + validateTriggerPrice (#4): market-detail review→confirm.
The review quotes the REAL fill and BLOCKS the sign if (a) the strike fails
validateTriggerPrice vs real entry [#4], (b) no takeProfitQuote, (c)
profitUsdUi≤0 (inside the round-trip spread), or (d) live payout <60% of shown
(stale-price divergence guard). Confirm re-quotes + re-checks before send; the
validated quote IS the signed tx (no TOCTOU). The venue's profitUsdUi is ground
truth — never trust local math over it.

Pure + LIVE verified; typecheck green. Banner stays up pending the 3-agent
re-review. Remaining: settlement (#5), gates (#6), buckets (#7).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(fix #6)

- canBet split from funding: 'enabled' (wallet+session) vs per-stake funding.
  An enabled-but-underfunded user now gets an 'Add funds to bet' ticket message
  instead of a dead-end 'enable' CTA. availableUsd threaded markets-app → detail.
- stake > balance blocked proactively (tooBig) + stake < MIN_STAKE (11) gate.
- enable() surfaces the REAL human-phrased step error (EnableState.error) that
  was blackholed by onStep:()=>{}; both enable + place route through calmError.
- double-submit guards: doReview refuses from an in-flight phase / invalid stake;
  doConfirm refuses while signing (belt-and-suspenders over the disabled button).

typecheck green. Banner stays up pending review. Remaining: settlement (#5),
buckets (#7).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gine (fix #7)

Investigation finding: a single capped-loss perp + one take-profit can only
express a ONE-TOUCH 'reaches X', never a bounded 'lands between X and Y' (needs a
call-spread = 2 positions). The honest relabel (de-normalized 'chance it reaches
this zone') was implemented but DEGENERATES: with edges inside the 5-10% spread
every near-edge sits below the entry fill, so every zone saturates to the 97c
ceiling — and it duplicates the strike ladder, which already gives honest
reach-odds.

Rather than ship a degenerate/dishonest widget on a real-funds path, the buckets
section is REMOVED from the rendered detail. bucketMarket stays in the engine
(corrected to honest non-normalized reach semantics) for a future call-spread or
vol-aware rebuild. FLAGGED: making the removal permanent vs. a call-spread
rebuild is a scope decision for the user.

typecheck green. Banner stays up. Remaining: settlement (#5), then 3-agent review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…x #5)

Makes the 'by <timeframe>' honest. A bet is a capped-loss perp + bundled TP, so
it resolves on-chain itself (TP=win, liquidation=lose); a bet that does NEITHER
by its deadline would ride forever and bleed borrow fees, so the engine CLOSES it
at mark at expiry — mirroring the proven Updown settleRound (explicit '0' full
close; reconcile on observed snapshot+clock, not a bare timer; inflight set +
'settling' status guard double-close).

- lib/use-market-rounds.ts: the engine. SEPARATE localStorage namespace
  ('predict-markets') from the Updown app so reconciliation can't cross-
  contaminate. toSettle (expired+open) -> close at mark, result = watched PnL.
  closedElsewhere (vanished) -> infer win/lose from live mark vs strike (win uses
  the reconciled win-profit; loss is the capped -stake).
- lib/rounds.ts: optional strike + winProfitUsd on Round; loadRounds/saveRounds
  take an optional key prefix (Updown path unchanged by default).
- components/bets.tsx (replaces portfolio.tsx): active bets show live value +
  countdown + manual settle; settled bets show WON/LOST.
- detail builds the Round on confirm (strike, expiresAt, reconciled win-profit);
  ticket footer now discloses the settlement rule.

typecheck + production build both green (exit 0).

FLAG for #10 review: (a) the closedElsewhere win/lose is a mark-vs-strike
heuristic (exact realized amount is on-chain); (b) cross-app edge — same wallet
trading the same market+side on both / and /markets shares one on-chain position.

②-⑧ COMPLETE. Next: the 3-agent adversarial review (#10), then the banner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… scoring)

3-agent review (code-reviewer + silent-failure-hunter + money-math auditor) found
real issues in the settlement engine. Fixed the clear, high-value ones:

- C1 (CRITICAL): reconcileBet now calls checkCollateralForTriggers(stake, entryFee)
  — blocks a stake that drops to <= after fees (the bundled TP, the entire win
  boundary, would silently fail to place on-chain).
- C2 (CRITICAL): on mount, demote any persisted 'settling' round -> 'active' so a
  tab-close mid-settle can't strand a position forever (reconcile only acts on
  'active').
- Settlement scoring (CRITICAL, display/stats): resolveVanished now RETURNS on a
  failed price fetch instead of branding a winner a total loss (mark==null ->
  -stake was the worst bug). Retry-safe.
- H1 (HIGH): doConfirm gains a synchronous useRef lock — a fast double-tap can no
  longer pass the async 'phase' guard twice and fire two real opens.
- H2 (HIGH): settleRound skips the close if the position is already gone (positionFor
  undefined) -> resolves as vanished, so a timeout-retry can't double-close.
- H4 (HIGH): the 'Position is empty' branch now SCORES the round (resolveVanished)
  instead of leaving a resultless 'closed-elsewhere' that vanishes from the UI.
- F2 (HIGH latent): deleted dead bucketMarket + openParamsFor — undefended exported
  paths that render a guaranteed-loss strike as 97c if re-wired.
- markets.ts: oracle<=0 now -> PROB_FLOOR (3c 'no'), not the 97c ceiling a zero
  entry painted (matches the doc); refs cleared on owner switch (M3); custody fetch
  checks res.ok before caching (rev2-H4).

typecheck + build green. BANNER STAYS UP. Still open for the user: F7 (cross-app
shared position — / and /markets share one on-chain position per market+side;
needs a product decision) + the residual vanish-scoring heuristic (exact realized
PnL is the on-chain receipt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er MEDIUM)

The re-review caught a regression I introduced: settleRound's gone-branches
flipped status -> 'closed-elsewhere' BEFORE resolveVanished, but reconcile only
re-emits 'active' rounds, so a null price-fetch in resolveVanished stranded the
round resultless forever (funds safe — position already closed on-chain; only the
local win/lose card/stat was lost).

Fix (single-writer transition): the gone-branches now LEAVE the round 'active' and
let resolveVanished own the active->settled transition. A failed score-fetch keeps
it 'active', so reconcile re-emits + retries it. No round is ever stranded
resultless. (No status writes 'closed-elsewhere' in the markets path anymore;
active->settled directly.)

Verifier confirmed 7/8 prior fixes correct + the doConfirm lock clean (finally
releases on early return). typecheck + build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
F7 (cross-app shared position): / (Updown) and /markets share one on-chain
position per market+side on a wallet, so one app's auto-settle could FULL-close
the other's bet. settleRound now takes an 'auto' flag: the expiry watcher
(auto=true) REFUSES to full-close a position whose on-chain size > 1.5x this
bet's recorded size (a blend tell-tale), leaving it 'active'; the user's explicit
manual settle (auto=false) is allowed through. Imperfect for small blends
(sub-account namespacing is the complete fix) — documented.

SPEC-PREDICT-V2.md: added the v2.1 REVIEW OUTCOME section — every fixed
CRITICAL/HIGH (C1,C2,scoring,H1,H2,F2,F7 + oracle/refs/res.ok), the one regression
caught+fixed, and the documented residuals that gate the banner.

typecheck + build green. Banner STAYS UP pending a final re-verify of the F7 guard
+ the product decision on whether the two example apps may share a wallet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nds; remove Updown

Wallet (was locked to one wallet): the connect flow force-selected wallets[0].
New WalletPicker lists EVERY installed wallet (Wallet Standard auto-detects
Phantom/Solflare/Backpack/Glow/…); nothing is hardcoded. Added disconnect/switch
(tap the address). wallet-adapter-react 0.15.39 already does Standard detection.

Routing (couldn't reach /markets without the URL): app/page.tsx now renders the
prediction app at /, /markets 307-redirects to /, layout title updated. The
prediction market is the homepage.

Standalone funds: ported deposit/withdraw (FundsSheet + lib/funds) into the
markets shell — tap the balance pill to deposit USDC. Markets no longer depends on
the old app for funding.

Removed the Updown app: deleted 13 Updown-only files (app, active-round,
enable-sheet, history, mechanics-disclosure, price-chart, settle-card, stats-strip,
ticket, market-picker, wallet-bar, lib/clock, lib/use-price-history). Verified the
delete closure is self-contained (every importer was itself Updown-only) and the
kept markets tree references none of them. BONUS: removing the second app
ELIMINATES the F7 cross-app shared-position hazard entirely.

typecheck + production build green; dev server fresh on :3000 (/ → 200 predict,
/markets → 307 →/).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The custom WalletPicker diverged from the house pattern and was unreliable: it
called select() and relied on autoConnect to race in (pick a wallet → often
nothing happens). Replaced with the SAME flow tap-trade uses:

- components/wallet-sheet.tsx: the proper wallet-adapter 'ref dance' — select() is
  async, so a wantConnect ref + an effect fire connect() once the adapter is
  actually selected. Handles not-installed wallets (open install URL), a
  connecting… state, a hydration guard (React #418), and closes on connect. Lists
  EVERY installed wallet via Wallet Standard. Predict glass styling.
- components/wallet-menu.tsx: the connected dropdown matching tap-trade's top-bar
  menu — balance (wallet usdc / in account) · deposit/withdraw · copy address ·
  disconnect.
- markets-app: 'Connect Wallet' → wallet sheet; connected → WalletMenu; deleted
  wallet-picker.tsx and the ad-hoc disconnect/address button.

typecheck + production build green; dev server fresh (/ → 200, 'Connect Wallet'
present).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y review)

The wallet-flow security review found one divergence from the proven tap-trade
pattern: predict's signer useMemo omitted tap-trade's authority guard. During an
A→B wallet switch, session state lags anchorWallet by one render, so
makeSessionSigner could briefly pair wallet B with wallet A's session token. The
review verified it was never consumed (auto-settle bails on the reset null
snapshot; all else is user-gated) — LOW/defense-in-depth — but it's the only thing
that kept predict from an exact match. Now guarded exactly like tap-trade
app.tsx:115. Review verdict: no CRITICAL/HIGH; correct, secure, faithful.

typecheck green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…after enable

Root cause (found by diffing against the working tap-trade app): enable.ts,
session.ts (bar the storage-key name), and signer.ts are all IDENTICAL to
tap-trade. The only divergence was the post-enable wiring. tap-trade's doEnable
does 'await refresh()' (the OWNER-STREAM refresh) after enabling so the new basket
lands; predict's enable() refreshed wallet balance + basket BALANCE but NEVER the
owner snapshot. So after a successful enable, snapshot.basketPubkey stayed null →
basketExists=false → canBet=false → the UI never left 'Enable one-tap' (the exact
stuck-after-enable bug the other apps already fixed).

Fix: markets-app now destructures refresh from useStream and 'await refreshSnapshot()'
after enable (+ after a placed bet, matching tap-trade app.tsx:373/580). The
StreamProvider already exposed refresh(); it just wasn't called.

typecheck green; dev server hot-reloaded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- default stake is now the actual floor (MIN_STAKE $11) instead of an arbitrary $25
- 'max' button → stakes the full available balance (floored to cents)
- input sanitizes to [0-9.]; placeholder shows the min
- helper line: 'min $11 (a bet bundles a take-profit) · you have $X' so the floor
  and balance are explicit (a sub-$11 stake can't carry the bundled TP on-chain)

Note: this does NOT change the $11 minimum — that's the >$10-after-fees rule for
the bundled take-profit. A $9.74 account is below it; deposit to ~$12 to bet.

typecheck green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Kaleb-Rupe Kaleb-Rupe merged commit 1872b98 into main Jun 15, 2026
1 check passed
@Kaleb-Rupe Kaleb-Rupe deleted the feat/predict-updown branch June 15, 2026 21:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant