Prediction Markets, LP Locks, and an Admin Page That Doesn’t Suck
How I bolted CPMM prediction markets onto ZeraSwap, locked LP for graduated tokens, and built a 5-tab admin panel before the first malicious actor showed up.
- FROM
- Dax the Dev <[email protected]>
- SOURCE
- https://blog.skill-issue.dev/blog/prediction_markets_admin/
- FILED
- 2026-02-18 19:31 UTC
- REVISED
- 2026-02-18 22:43 UTC
- TIME
- 6 min read
- SERIES
- Zera Origin Stories
- TAGS
A week after the AMM shipped I had two open feature requests from people who were actually using it:
- “I want to bet on whether $TOKEN graduates by Friday.”
- “Why doesn’t the launchpad lock LP after graduation? You’re going to get rugged.”
Both fair. Both were addressed in 16aa30d — Add prediction markets, LP locking, graduation flow, comprehensive admin, and USD pricing on 2026-02-18. 55 files changed. Let’s unpack the parts that actually matter.
Prediction markets as a CPMM
A prediction market is just a CPMM with two outcome reserves instead of a token + SOL pair. From sdk/src/prediction_math.ts:
// CPMM: shares_out = outcome_reserves * sol_after_fee
// / (other_reserves + sol_after_fee)
export function calcBuyOutcome(
solIn: bigint, yesReserves: bigint, noReserves: bigint,
outcome: "yes" | "no", feeBps: bigint,
): { sharesOut: bigint; fee: bigint } {
const fee = (solIn * feeBps) / BPS_DENOMINATOR;
const solAfterFee = solIn - fee;
const outcomeReserves = outcome === "yes" ? yesReserves : noReserves;
const otherReserves = outcome === "yes" ? noReserves : yesReserves;
if (outcomeReserves === 0n) return { sharesOut: 0n, fee };
const sharesOut =
(outcomeReserves * solAfterFee) / (otherReserves + solAfterFee);
return { sharesOut, fee };
}
// YES price = no_reserves / (yes_reserves + no_reserves)
export function calcOutcomePrice(yesReserves, noReserves) {
const total = yesReserves + noReserves;
if (total === 0n) return { yesPrice: 0.5, noPrice: 0.5 };
// ...
}
The trick is the price. In a YES/NO CPMM the price of YES is just the ratio of NO reserves to total reserves. That’s because if YES is “expensive” (lots of YES shares already sold), there’s less YES reserve left, and the next dollar buys you fewer YES shares. The math is symmetric.
I picked CPMM over LMSR because:
- The LP doesn’t need to subsidize liquidity. Whoever creates the market puts up real SOL on both sides and earns the fees.
- It uses literally the same
x*y=kengine as ZeraSwap’s swap path, so I could reuse the slippage andMathOverflowchecks I’d already debugged. - Resolution is a single instruction that drains the losing side into the protocol and pays out the winning side proportionally.
Six instructions on-chain: create_market, buy_outcome, sell_outcome, resolve_market, claim_winnings, void_market (plus protocol fee collection). The void path is the safety valve — if the resolution oracle disappears or the market becomes ambiguous, the admin can void and refund pro-rata.
LP locking: the part that actually makes graduation safe
Before this commit, when a launchpad token graduated to a real ZeraSwap pool, the LP tokens were minted to the launchpad authority and that was that. Nothing stopped the launch creator from yanking liquidity 30 seconds later. Classic rug.
The fix is LpLock PDA + lock_liquidity and extend_lock instructions, and a check in remove_liquidity that consults the lock state. Now graduation locks the launch’s LP for a configurable window. If you want to be a serious launch, you opt into a longer lock; the frontend surfaces the lock duration as a trust signal on the explore page.
I shipped a related quality-of-life thing the same day in a02f672 — lower graduation to 50 SOL. 85 SOL was the original threshold and nobody could actually graduate a token at $15K worth of bonding-curve liquidity. 50 SOL turned out to be the floor where a real microcap launch could clear graduation.
The 5-tab admin page
The same commit ships a five-tab admin panel: Overview / Launchpad / AMM / Markets / Docs. The reason this is its own thing is not vanity — it’s that a Solana program with five separate config PDAs and three separate fee vaults cannot be safely operated from a CLI. You will misread a hex address. You will paste the wrong network. You will pause production thinking it’s devnet.
Each tab carries:
- All three vault balances with USD denomination (SOL/USD pulled from CoinGecko via
SolPriceContextpolling). - “Initialize PDA” buttons for any config that hasn’t been bootstrapped on the current cluster.
- Per-launch / pool / market fee collection, plus a “collect all” bulk button.
- The void-market button on the prediction tab, behind a confirm modal, because the void path is irreversible.
I ended up needing this faster than I expected. The very next day I shipped 557d314 — Add migrate_config instruction for safe account resizing and f673b22 — Add config migration UI to admin page. The trigger: I’d added a min_market_liquidity field to PredictionConfig without bumping the account size, and existing configs on devnet couldn’t take the update. The admin page detected old-format accounts via a length comparison and surfaced a “Migrate Config” button.
migrate_config does what its name says — resizes the account, copies the old data, writes the new field. The trick I missed the first time, fixed in 6d04415: when growing a PDA you have to fund the lamport difference via a System Program CPI transfer, not by directly debiting the user’s lamports inside the program. Anchor will let you write the second one. The runtime will reject it. Welcome to Solana.
Trade-offs
Why CPMM and not parimutuel pools? Because parimutuel doesn’t give you a price until resolution. CPMM lets traders see “YES is at 67¢” continuously. That’s the entire UX of a prediction market. If you can’t show a price, your users are going to ask why they shouldn’t just use Polymarket.
Why void-market behind admin only? Because the alternative is “anybody can vote to void a market they’re losing” and that destroys the incentive to make confident bets. The market creator stakes the liquidity; the protocol admin holds the void key. The doc tab on the admin panel makes that policy explicit.
Why an admin page in a “decentralized” project? Because the project isn’t decentralized yet. I’m not going to pretend it is. The admin keys exist; they’re documented; they will be migrated to a multisig, and eventually to TW-TVV-style governance (described in the m0n3y origin post). Lying about that today doesn’t make it true tomorrow.
What this taught me
The smart-contract surface of a Solana product compounds non-linearly. ZeraSwap had three PDAs and one fee vault. Adding prediction markets and LP locks brought it to seven PDAs and three fee vaults. The cost of ad-hoc admin tooling exploded. The 5-tab admin page paid for itself in the first hour after deploy when I needed to bulk-collect fees from 12 launches.
Further reading
- The full prediction-markets commit
prediction_math.tsmigrate_configinstruction (the safe-resize fix)- Polymarket — the UX target nobody on Solana matches yet
- LMSR vs CPMM market makers — the paper that justifies LMSR for thin markets