DOC# PREDIC SLUG prediction_markets_admin PRINTED 2026-05-06 03:47 UTC

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
#zera #solana #anchor #prediction-markets #cpmm #admin #governance

A week after the AMM shipped I had two open feature requests from people who were actually using it:

  1. “I want to bet on whether $TOKEN graduates by Friday.”
  2. “Why doesn’t the launchpad lock LP after graduation? You’re going to get rugged.”

Both fair. Both were addressed in 16aa30dAdd 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:

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 a02f672lower 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:

I ended up needing this faster than I expected. The very next day I shipped 557d314Add migrate_config instruction for safe account resizing and f673b22Add 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

← Back to article