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.
financial_event row on the shared Neon ledger. No off-ledger revenue. Public Merkle verifier at /verify proves inclusion of any settlement event.
| Product | Fee model | Charged when | Ledger event |
|---|---|---|---|
| CLOB (limit orders) | 30 bps taker · 2 bps maker rebate · 5-actor split | At fill | FEE_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 capture | At settle (haircut on winner payout) | FEE_AMM_HOUSE (per winning position) |
| AMM / fast round (v2) | Same edge, baked into displayPrice | At buy (inflated purchase price) | FEE_AMM_HOUSE (per buy) |
| Parlay | 15% parlay_margin (baked into odds at placement) | At settle (winner receives pre-discounted payout) | FEE_PARLAY_HOUSE (per won bet) |
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.
| Tier group | acq | exec | op | mc | lp | plat_net | Builder combined | % of fee |
|---|---|---|---|---|---|---|---|---|
| Auto (self_serve / read_live / trade_capped) | 6 | 6 | 3 | 5 | 7 | 3 | 12 bps | 40% |
trade_full (admin) | 7 | 7 | 3 | 4 | 6 | 3 | 14 bps | 47% |
genesis / partner (admin) | 8 | 8 | 2 | 4 | 5 | 3 | 16 bps | 53% |
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.
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:
| Price | Flat mode | Dynamic mode |
|---|---|---|
| 0.50 | 30 bps | 30.00 bps |
| 0.70 | 30 bps | 25.20 bps |
| 0.90 | 30 bps | 10.80 bps |
| 0.99 | 30 bps | 1.19 bps |
Rates (from riskSettings, admin-tunable via /admin/risk-control):
house_edge: 2% default, dynamic 0.5-8% (via computeDynamicHouseEdge() — higher at extreme prices)spread: 4% default, dynamic 2-8%lp_platform_share, lp_holder_share, lp_insurance_share)Two pricing models coexist (transition):
× (1 - houseEdge). Edge captured at settle. Positions tagged _priceModelV2 = false.displayPrice = buyPrice / (1 - settleFee); winning shares payout $1 flat. Edge captured at buy. Positions tagged _priceModelV2 = true.Ledger events (2026-09-06+): Both v1 and v2 write FEE_AMM_HOUSE rows to financial_event:
server.js writes per winning position (lots × houseEdge). Idempotency key fee_amm:<marketId>:<roundId>:<userId>:<side>._executeTradeInternalRaw writes at trade placement (notional × houseEdge). Idempotency key fee_amm_v2:<marketId>:<roundId>:<userId>:<side>:<ts>.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.
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.
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:
FEE_LP total from Neon.lp_positions from supabase2.shares.lp_positions.total_earned + writes lp_pnl_log audit rows (pseudo-round clob_lp_dist_<ts>).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.
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.
User's balance debited via balanceManager.debit; ledger accounting entry writes to platform:net.
feeSplitPoller (CLOB, every 30s) or inline settle hook (AMM/parlay) writes typed financial_event rows on Neon.
Events sit unsettled (settled_at IS NULL) until claimed. Per-actor sums queryable via /v1/attribution/fills and /v1/revenue.
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.
Settle endpoint creates a withdrawal_state row with source='builder_earnings' / 'creator_earnings'. Existing withdrawal pipeline delivers USDT to the wallet address.
| event_type | Debit account | Credit account | Source | Notes |
|---|---|---|---|---|
FEE_ACQUISITION_BUILDER | platform:hold | builder_acq:<user_id> | CLOB feeSplitPoller | 6-8 bps by tier |
FEE_EXECUTION_BUILDER | platform:hold | builder_exec:<user_id> | CLOB feeSplitPoller | 6-8 bps by tier |
FEE_OPERATOR | platform:hold | operator:<user_id> | CLOB feeSplitPoller | 3 bps (usually PAX = op_pax_v3) |
FEE_MARKET_CREATOR | platform:hold | creator:<user_id> | CLOB feeSplitPoller | 4-5 bps |
FEE_LP | platform:hold | pool:lp | CLOB feeSplitPoller | 5-7 bps; distributed via lpFeeDistributionCron |
FEE_PLATFORM_NET | platform:hold | platform:net | CLOB feeSplitPoller | 3 bps + absorbed unfilled attribution |
FEE_AMM_HOUSE | user:<user_id> | platform:net | Fast-round settle (v1) OR trade place (v2) | Approx notional × houseEdge |
FEE_PARLAY_HOUSE | user:<user_id> | platform:net | Parlay settle (WON bets only) | effective_payout × parlay_margin |
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.
/v1/fees/estimate, /v1/attribution/*, /v1/revenue/*, /v1/builders/me/settle, /v1/admin/{builders,creators}/:id/settle