Architecture at a glance

flowchart TD
    CreatePage[/create page/] -->|mechanism: cpmm-1 or dpm-2| MarketAPI[POST /v0/market]
    MarketAPI --> NewContract[getNewContract]
    NewContract --> BinaryCpmm[getBinaryCpmmProps]
    NewContract --> BinaryDpm[getBinaryDpmProps]

    PlaceBet[POST /v0/bet] --> Dispatch{contract.mechanism}
    Dispatch -->|cpmm-1| CpmmBet[getBinaryCpmmBetInfo]
    Dispatch -->|cpmm-multi-1| MultiBet[getNewMultiCpmmBetInfo]
    Dispatch -->|dpm-2| DpmBet[getBinaryDpmBetInfo]

    MarketPage[Market page] -->|creator, dpm-2 only| ConvertAPI[POST /v0/convert-dpm-to-cpmm]
    ConvertAPI --> ConvertHelper[dpmToCpmmHelper]
    ConvertHelper -->|rewrite shares, rebuild metrics, update pool, set mechanism| DB[(contracts, contract_bets, user_contract_metrics)]

    ResolveAPI[POST /v0/market/.../resolve] --> ResolveGuard{mechanism == dpm-2?}
    ResolveGuard -->|yes| Reject[APIError: convert first]
    ResolveGuard -->|no| Resolve[resolveMarketHelper]

Approach summary

  1. Add dpm-2 as a live mechanism: new DPM & Binary variant on Contract, new math module, new branch in place-bet, unchanged Bet row shape (DPM bets use the same shares/amount/outcome/limitProb/fills fields as CPMM).
  2. DPM CLOB: implement the full Phase 1 / Phase 2 / Phase 3 pinned-price matching from the explainer, in a new computeDpmFills analogous to CPMM's computeFills. Limit orders live in the same contract_bets table with limitProb / orderAmount — same schema that CPMM already uses.
  3. 99% price cap: enforced in computeDpmFills by clamping DPM-curve segments so probability never crosses 0.01–0.99. Refund any residual order amount beyond the cap.
  4. Conversion: in-place rewrite of every filled DPM share amount to its post-conversion token count (shares *= C/y for YES, shares *= C/n for NO), rebuild user_contract_metrics from the rewritten bets, seed the new CPMM pool entirely from the ante's tokens using the Maniswap curvature formula, flip mechanism to 'cpmm-1', leave resting limit orders untouched (prices and budgets coincide per the migration doc).
  5. Resolution: early-reject any attempt to resolve a dpm-2 market.
  6. PnL / portfolio / leagues: thread dpm-2 through every payout and metrics helper using a pool-weighted mark-to-market. Profile "profit", portfolio-history graph, per-position PnL, day/week/month deltas, and league mana_earned all share a handful of common helpers (calculatePayout, getProfitMetrics, calculatePayoutFromShares, calculateMetricsFromProbabilityChanges, calculateProfitMetricsAtProbOrCancel) that currently either fall through to bet.amount or throw for non-CPMM mechanisms. Each needs a dpm-2 branch so DPM activity is visible across all surfaces during the DPM phase.
  7. UI: binary-only Classic/DPM toggle on /create; on the market page hide the sell UI (already implicit — sell is gated on cpmm-1) and render a "Convert to Classic" panel visible only to the creator (and admins/mods) when mechanism === 'dpm-2' && !isResolved. Hide the sell tab and the "sell shares" button for DPM. The frontend work needs a broader audit than BuyPanel because several binary views and helpers special-case cpmm-1.

Legacy DPM code handling: the tree has no live DPM runtime (common/src/calculate-dpm.ts is absent; new-contract.ts has a commented-out DPM block; old migration scripts reference deleted code). We will introduce a fresh calculate-dpm.ts with only the math this feature needs and delete the stale comment block. Old migration scripts in backend/scripts/ remain untouched (they reference an old-shape DPM model that no longer applies; they were already broken pre-feature). We also need to avoid inheriting fixed-payout-only behavior from the current CPMM stack, especially redeemShares() and any binary helpers that assume cpmm-1.


Phase 1 — Common types and math

1.1 Add DPM shape to common/src/contract.ts

Add a new DPM record that reuses the existing Binary intersection pattern. Include it in AnyContractType, MarketContract, and export convenience aliases.

export type DPM = {
  mechanism: "dpm-2";
  pool: { YES: number; NO: number };
  initialPool: { YES: number; NO: number };
  initialProbability: number;
  prob: number;
  probChanges: { day: number; week: number; month: number };
  totalLiquidity: number;
  subsidyPool: 0;
};
export type DPMContract = Contract & DPM;
export type BinaryContract = Contract & Binary;

Extend AnyContractType to include (DPM & Binary) and extend MarketContract / tradingAllowed predicates so DPM is considered a trading mechanism. Also audit helper unions that currently assume all binary trading markets are cpmm-1, such as common/src/calculate.ts, web/components/contract/contract-price.tsx, web/components/bet/user-bet-summary.tsx, and web/components/bet/limit-orders-table.tsx.

1.2 New file common/src/calculate-dpm.ts

Pure functions for DPM math. No state, no I/O.

export const dpmCost = (y, n) => Math.sqrt(y * y + n * n)

export const getDpmProbability = (pool: { YES: number; NO: number }) =>
  pool.YES ** 2 / (pool.YES ** 2 + pool.NO ** 2)

export const dpmInitialPool = (ante: number, initialProb: number) => ({
  YES: ante * Math.sqrt(initialProb),
  NO:  ante * Math.sqrt(1 - initialProb),
})

// b = sqrt((y+s)^2 + n^2) - sqrt(y^2 + n^2); solve for s
export const dpmBuyShares = (pool, outcome, amount): {
  shares: number; newPool: typeof pool
} => { ... }

// Max YES shares that can be bought before prob reaches p*:
//   s_max = n * sqrt(p*/(1-p*)) - y  (symmetric for NO)
export const dpmSharesToReachProb = (pool, targetProb, outcome): {
  shares: number; cost: number
} => { ... }