Fee Architecture

Every fee PAX charges, exactly where it's captured, exactly where it goes. No hand-waving — file paths, ledger event types, bps values, and payout mechanics for auditors, builders, and VC due-diligence.

Design principle: a single revenue lever (per-trade fee) with four distinct product surfaces (CLOB / AMM / parlay / LP-share). Every taken bps is recorded as a typed financial_event row on the shared Neon ledger. No off-ledger revenue. Public Merkle verifier at /verify proves inclusion of any settlement event.

Four product surfaces, one ledger

ProductFee modelCharged whenLedger event
CLOB (limit orders)30 bps taker · 2 bps maker rebate · 5-actor splitAt fillFEE_ACQUISITION_BUILDER · FEE_EXECUTION_BUILDER · FEE_OPERATOR · FEE_MARKET_CREATOR · FEE_LP · FEE_PLATFORM_NET
AMM / fast round (v1)2% house_edge (dynamic 0.5-8%) + spread captureAt settle (haircut on winner payout)FEE_AMM_HOUSE (per winning position)
AMM / fast round (v2)Same edge, baked into displayPriceAt buy (inflated purchase price)FEE_AMM_HOUSE (per buy)
Parlay15% parlay_margin (baked into odds at placement)At settle (winner receives pre-discounted payout)FEE_PARLAY_HOUSE (per won bet)
CLOB

Layer 1 — CLOB order-book taker fee

Base rate: 30 bps (0.3% of fill notional). Cut from 50 bps on 2026-09-06 to attract CLOB volume.

Code: lib/clobFees.js (charge path in services/clobEngine.js) + lib/fiveActorFees.js (split path via services/feeSplitPoller.js). Both files declare TAKER_FEE_BPS_BASE — a boot-time [fee-sync-guard] in server.js asserts equality and refuses startup on mismatch.

Progressive tier — builder share grows with tier

Tier groupacqexecopmclpplat_netBuilder combined% of fee
Auto (self_serve / read_live / trade_capped)66357312 bps40%
trade_full (admin)77346314 bps47%
genesis / partner (admin)88245316 bps53%

Unfilled roles: if acquisition_builder_id or market_creator_id is null on a trade, that share flows to platform_net. Every trade's 30 bps is always fully allocated.

Dynamic fee (env-gated)

When PAX_DYNAMIC_FEE_ENABLED=on, CLOB taker fee scales as fee = notional × rate × price × (1 - price) × 4 — matches Polymarket + Kalshi industry-standard formula. Max fee (30 bps at rate=0.003) at 50/50 markets; approaches 0 at extremes:

PriceFlat modeDynamic mode
0.5030 bps30.00 bps
0.7030 bps25.20 bps
0.9030 bps10.80 bps
0.9930 bps1.19 bps
AMM

Layer 2 — AMM / fast-round house edge + spread

Rates (from riskSettings, admin-tunable via /admin/risk-control):

Two pricing models coexist (transition):

Ledger events (2026-09-06+): Both v1 and v2 write FEE_AMM_HOUSE rows to financial_event:

Builder attribution: at trade placement, _executeTradeInternalRaw checks builder_registry — if the trading user is a registered builder, _executionBuilderId is set on the position and carries through to both the buy-time and settle-time FEE_AMM_HOUSE events.

PARLAY

Layer 3 — Parlay margin

Rate: parlay_margin = 0.15 (15%) — industry standard for correlated multi-leg risk (FanDuel avg 23.5%, DraftKings 17.7%, rest of market 14.7%).

Mechanic: margin is baked into odds at PLACEMENT — potential_payout = stake / (Π(probs) × (1 - dynamicMargin)). On WIN, user receives the pre-discounted potential_payout; house implicitly captures the fair-vs-discounted difference. On LOSE, stake is retained as game outcome (not fee).

Ledger event (2026-09-06+): FEE_PARLAY_HOUSE is written on WON bets only (approximation effective_payout × parlay_margin). Idempotency key fee_parlay:<bet_id>. LOST bets get no fee event (game outcome, not fee).

Builder attribution: parlay_bets.execution_builder_id column populated at place time via builder_registry lookup on the user_id. Historical bets remain null.

LP

Layer 4 — LP holder distribution

Source: the FEE_LP events written on every CLOB fill (7 bps at auto tier, 5-6 bps at higher tiers). Accumulates in financial_event with credit_account = 'pool:lp'.

Distribution: services/lpFeeDistributionCron.js runs hourly:

  1. Aggregates unsettled FEE_LP total from Neon.
  2. Reads active lp_positions from supabase2.
  3. Distributes pro-rata by shares.
  4. Updates lp_positions.total_earned + writes lp_pnl_log audit rows (pseudo-round clob_lp_dist_<ts>).
  5. Marks the source FEE_LP events settled with the distribution ID.

Env knobs: LP_FEE_DISTRIBUTION_ENABLED (on/off, default off), LP_FEE_DISTRIBUTION_INTERVAL_MS (default 3600000 = 1 h), LP_FEE_DISTRIBUTION_MIN_USD (default 0.10). No active LP positions? Events settle to lp_dist_noholders_<ts> bucket — fees stay with platform_net.

Note on AMM/spread LP share (12% via lp_holder_share): that share flows through the pre-existing lp_pnl_log per-round mechanism at fast-round settle time (unchanged). The CLOB FEE_LP flow above closes the gap where CLOB LP share was accumulating in ledger but never reaching holders.

LP quality scoring — quadratic distance-from-mid

Raw FEE_LP distribution alone rewards volume regardless of spread tightness. A separate rollup (lib/lpPositionsRollup.js, hourly) computes a quality-weighted lp_units score per LP per market per ISO week. Formula:

quality       = SUM( size × max(0, 1 - min(1, |distance_from_mid| × 4))² )
share         = user_quality / market_total_quality × 100
spread_bonus  = clamp(share / 100, 0, 0.5)              // caps at +50%
lp_units      = rebate_earned_usdt × (1 + spread_bonus)

Quadratic penalty: an LP quoting at the mid (distance_from_mid = 0) gets quality = size × 1² = size. At 5 cents from mid (dist = 0.05) the multiplier is (1 - 0.20)² = 0.64. At 25 cents from mid (dist ≥ 0.25) the multiplier collapses to 0 — no quality credit. This forward-compatible bonus surfaces automatically as spread scoring lands per market; the rebate_earned_usdt base flow is unchanged.

Read your LP rollup via GET /v1/mm/me/lp-positions?week=YYYY-Www — returns per-market breakdown + weekly totals. Raw signed inventory + risk limits at GET /v1/mm/me/inventory and GET /v1/mm/me/risk-limits.

Payout flow — from ledger to wallet

1. Trade fill

User's balance debited via balanceManager.debit; ledger accounting entry writes to platform:net.

2. Poller / hook

feeSplitPoller (CLOB, every 30s) or inline settle hook (AMM/parlay) writes typed financial_event rows on Neon.

3. Accrual

Events sit unsettled (settled_at IS NULL) until claimed. Per-actor sums queryable via /v1/attribution/fills and /v1/revenue.

4. Claim

Builder: POST /v1/builders/me/settle (self-serve) or POST /v1/admin/builders/:id/settle. Creator: POST /v1/admin/creators/:id/settle. LP holders: automatic hourly cron.

5. Withdrawal

Settle endpoint creates a withdrawal_state row with source='builder_earnings' / 'creator_earnings'. Existing withdrawal pipeline delivers USDT to the wallet address.

financial_event taxonomy

event_typeDebit accountCredit accountSourceNotes
FEE_ACQUISITION_BUILDERplatform:holdbuilder_acq:<user_id>CLOB feeSplitPoller6-8 bps by tier
FEE_EXECUTION_BUILDERplatform:holdbuilder_exec:<user_id>CLOB feeSplitPoller6-8 bps by tier
FEE_OPERATORplatform:holdoperator:<user_id>CLOB feeSplitPoller3 bps (usually PAX = op_pax_v3)
FEE_MARKET_CREATORplatform:holdcreator:<user_id>CLOB feeSplitPoller4-5 bps
FEE_LPplatform:holdpool:lpCLOB feeSplitPoller5-7 bps; distributed via lpFeeDistributionCron
FEE_PLATFORM_NETplatform:holdplatform:netCLOB feeSplitPoller3 bps + absorbed unfilled attribution
FEE_AMM_HOUSEuser:<user_id>platform:netFast-round settle (v1) OR trade place (v2)Approx notional × houseEdge
FEE_PARLAY_HOUSEuser:<user_id>platform:netParlay settle (WON bets only)effective_payout × parlay_margin

Independent verification

Every financial_event row is hash-chained into the operational audit log and Merkle-batched. The public Merkle verifier at /verify exposes six no-auth endpoints (/v1/audit/status, /batches/latest, /batches/{num}, /events/{seq}, /proof/{seq}, /anchor/{num}) so any third party can independently confirm that a specific fee event was recorded and included in a signed, R2-anchored batch — no PAX cooperation required.

References