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.

For traders — 60-second version: When you trade on the CLOB, you pay a taker fee between 0.08% and 0.25% of notional (peaks near 50/50 markets, drops at extremes because uninformed edge is smaller). Market makers who post resting limit orders get paid — a 0.03-0.08% rebate scaled by their liquidity quality score. Every trade splits automatically into 7 buckets — the acquisition builder (3-5 bps), execution builder (3-5 bps), operator (1-2 bps), market creator (2-3 bps), the per-market MM stipend pool (4-5 bps), LP holders (1-2 bps), and platform (2 bps). No spread markups, no hidden fees, no monthly subscription. Every bps flows to a typed financial_event row on the ledger — you can independently verify at /verify, and live per-market config is queryable at the public LQF API.
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)PAX Adaptive Fee Curve (PAF): 8-20 bps effective taker (peak at 50/50 = PAF_BASE=20; floor 8 bps at extremes; 25 bps ceiling is a dormant configurable cap) · quality-scaled 3-8 bps maker rebate · 7-actor splitAt fillFEE_ACQUISITION_BUILDER · FEE_EXECUTION_BUILDER · FEE_OPERATOR · FEE_MARKET_CREATOR · FEE_MM_STIPEND · FEE_LP · FEE_PLATFORM_NET
AMM / fast round (v1)base_rate × p × (1 − p), base_rate = 5% → max 1.25% effective at 50/50, drops symmetrically to 0 at 0¢/100¢At settle (haircut on winner payout)FEE_AMM_HOUSE (per winning position)
AMM / fast round (v2)Same base_rate × p × (1 − p), 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 adaptive taker fee (PAF)

PAX Adaptive Fee Curve (PAF). Instead of a flat rate, taker fee scales with information asymmetry: at max uncertainty (50/50) the fee peaks; at settled-outcome extremes (near 0¢ / 100¢) the fee floors. This aligns cost with informed-trader edge without giving away free trades on stale prices.

Formula: raw_bps = PAF_BASE × 4 × price × (1 - price), clamped to [PAF_FLOOR, PAF_CEILING]. Because 4·p·(1−p) peaks at 1 when p = 0.5, the raw curve maxes out at PAF_BASE_BPS. With default constants, the effective per-fill taker fee is 8–20 bps; the ceiling is a configurable safety cap that is dormant unless PAF_BASE is raised above 20.

PriceRaw curveEffective (after clamp)
0.5020.00 bps20 bps
0.7016.80 bps16.80 bps
0.90 7.20 bps8 bps (floor)
0.99 0.79 bps8 bps (floor)

Maker rebate: quality-scaled 3-8 bps. Formula: rebate_bps = 3 + 5 × lqf_score, where lqf_score ∈ [0, 1] comes from the LQF snapshot cron (see Liquidity Quality Framework below). Top-quality MMs get 8 bps rebate; new / thin-book MMs get the 3 bps floor.

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

Progressive tier — 7-actor split (shares of the effective clamped fee)

The bps figures below are defined at the PAF centre (effective fee = 20 bps at 50/50). For any given fill, each actor's absolute allocation is share_bps × (effective_bps / PAF_BASE_BPS) — so at the 8 bps floor, each bucket is scaled by 8/20 = 0.4. The sum of allocations always equals the effective per-fill fee — no bucket is subsidised. Call POST /v1/fees/estimate for the authoritative per-bucket bps of any given trade.

Tier groupacqexecopmcmm_stipendlpplat_netTotal @ centre
Auto (self_serve / read_live / trade_capped)332352220 bps
trade_full (admin)441351220 bps
genesis / partner (admin)551241220 bps

New bucket: mm_stipend_pool — 4-5 bps at PAF centre (scaled by effective fee at extremes) accrues to per-market MM reward pools, distributed daily to registered MMs via the LQF quality score (see below). Replaces the old flat 2 bps maker rebate with a quality-weighted, pool-based payout that better rewards tight-spread, two-sided, long-uptime market making.

Unfilled roles: if acquisition_builder_id or market_creator_id is null on a trade, that bucket's share flows to platform_net. Every trade's PAF fee is always fully allocated across the 7 actors and the sum equals the effective per-fill taker fee — verifiable via Σ financial_event.amount_usdt = fill.taker_fee_usdt per trade_id.

AMM

Layer 2 — AMM / fast-round symmetric fee

Formula (from lib/fastMarketFee.js — baseEdge(), live 2026-09-21 "Option A"):

fee = notional × base_rate × p × (1 − p) × (1 − vip_discount) × (1 − pax_discount)

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 (2 bps at auto tier, 1 bps at trade_full/genesis/partner). Accumulates in financial_event with credit_account = 'pool:lp'. The bucket size is fixed by tier under PAF — see the 7-actor table in Layer 1 for the exact tier-scaled breakdown. Legacy note: earlier drafts referenced 7/5-6 bps for LP; that predated the mm_stipend_pool bucket and is no longer correct.

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.

Liquidity Quality Framework (LQF) — 3-layer MM scoring

PAX rewards market makers on quality, not just volume. Every 60 s, an autonomous snapshot cron scores every resting MM quote across four dimensions; every 24 h, each market's mm_stipend_pool pays out pro-rata to top scorers. Snapshotting is fully independent of quote-post events — MMs cannot game the score by posting-and-cancelling.

3-layer score
// Layer 1 — Proximity (quadratic decay from filtered fair price)
proximity(order) = max(0, 1 - |price - filtered_fair_price| / max_qualifying_spread)²

// Layer 2 — Depth (proximity-weighted resting size per side)
depth_side(s) = Σ size_i × proximity(order_i)
q_bid = depth_side(YES-BID) + depth_side(NO-ASK)
q_ask = depth_side(YES-ASK) + depth_side(NO-BID)

// Layer 3 — Balance (two-sided requirement, harsher at extremes)
if filtered_fair_price ∈ [0.10, 0.90]:
    balance_quality = max(min(q_bid, q_ask),
                          max(q_bid, q_ask) / single_sided_penalty_ratio)
else:
    balance_quality = min(q_bid, q_ask)   // extreme prices require both sides

// Layer 4 — Continuity (time in book / snapshots seen)
continuity(u) = snapshots_present / snapshots_expected

// Final
market_score(u, m)  = balance_quality × continuity
user_share(u, m)    = market_score(u, m) / Σ market_score(*, m)
payout(u, m, epoch) = user_share × market_liquidity_stipend(m, epoch)

Per-market config lives in market_lqf_config: max_qualifying_spread_bps (default 500 = 5¢), depth_floor_usdt (default $5000), single_sided_penalty_ratio (default 3.0), daily_stipend_usdt.

Filtered fair price (dust-resistant)

Instead of naive mid-of-best-bid-ask, LQF computes fair price only over quotes with notional ≥ DUST_MIN_USDT (default $10). This prevents 1-share dust orders from moving the reference and thereby manipulating everyone else's proximity score.

Snapshot & payout schedule
Reading your score

Real-time forecast per MM: GET /v1/mm/stipend/estimate?market_id=…. Historical: GET /v1/mm/stipend/history?from=…&to=…. Raw inventory + risk limits stay at GET /v1/mm/me/inventory and GET /v1/mm/me/risk-limits.

LQF v2 — 4 顶级 upgrades (2026-09-24)

Four capability upgrades ship on top of the LQF v1 above. All default off (opt-in via platform_settings) so v1 behavior is preserved until each is deliberately enabled.

① Per-market boost_multiplier

New column market_lqf_config.boost_multiplier (default 1.0, range [0.1, 10.0]). When platform_settings.lqf.boost_enabled = true, the snapshot cron multiplies proximity_weighted_size × boost — lets admins spot-subsidize hot markets without changing the daily stipend pool (which accrues organically from taker fees, not from boost). Admin API: PUT /api/admin/market-lqf/:market_id/boost with { boost_multiplier, reason, expires_at }. Every change writes an audit line. Reason string is retained on market_lqf_config.boost_reason for exchange/VC audits.

② Rolling 1-hour uptime enforcement

New column market_lqf_config.min_hourly_uptime_pct (default 0 = disabled, range [0, 100]). When platform_settings.lqf.hourly_uptime_enforce = true and this per-market threshold is set (regulated-venue standard is typically 98%), the snapshot cron writes mm_hourly_presence rows tracking per-user per-hour snapshot counts. The daily payout cron reads those buckets — hours with snapshot_count < lqf.hourly_min_snapshots (default 45 of ~60/hr expected) are excluded. If actual uptime % falls below min_hourly_uptime_pct, that user's continuity for the epoch is zeroed. Closes the gap where a v1 MM could average 33% uptime (8h/day) and still collect from every 1h window.

③ Market Maker Agreement (MMA) whitelist tier

New table mm_agreement (user_id, series_id, tier, series_cap_usdt_weekly, hourly_uptime_pct, markets_pattern, active, granted_by, expires_at, disclosure_public). High-touch MMs bypass the general daily pool and go through a weekly series-level cap instead — default $50,000 per series per week. Series = SQL LIKE pattern over market_id (e.g. btc-1m%, sports-epl-%). When platform_settings.lqf.mma_enabled = true, the general daily payout cron excludes MMA holders (prevents double-dip), and a separate weekly cron mmAgreementWeeklyPayoutCron runs Monday 00:20 UTC to pay them from the same underlying pools. Idempotent via UNIQUE (user_id, series_id, week_start_ms). Failing the hourly uptime requirement → status='uptime_fail', payout = 0.

Public disclosure default = on. Individual agreements can opt-out via disclosure_public=false, but the base position is that every MMA term is queryable at GET /v1/lqf/mma-tier.

④ Public LQF disclosure API

Five no-auth endpoints let any trader / MM / auditor query the current LQF state without an API key:

EndpointReturns
GET /v1/lqf/configList all active LQF markets (paginated, ordered by daily_stipend_usdt desc)
GET /v1/lqf/config/:market_idFull per-market config + formulas + reference URL
GET /v1/lqf/pool/:market_idCurrent pool balance (accumulated_usdt / distributed_usdt / balance_usdt) + last epoch payout
GET /v1/lqf/leaderboard?days=7Top 50 MMs by past-week payout across all markets
GET /v1/lqf/mma-tierPublic MMA disclosures (opt-in per row) + formula reference

Emergency-off via platform_settings.lqf.public_disclosure_endpoint = false — endpoints return 503. Design principle: MMs deserve to see the rules they're competing under BEFORE committing capital.

v2 kill switches (all in platform_settings, 30 s hot-reload)
KeyDefaultBehavior when off
lqf.boost_enabledfalseSnapshot cron treats boost_multiplier as 1.0 regardless of DB value
lqf.hourly_uptime_enforcefalsePayout cron uses v1 unique_minutes / 1440 continuity (all hours count)
lqf.hourly_min_snapshots45Min snapshots per hour for it to count as "present" (out of ~60 expected)
lqf.mma_enabledfalseMMA rows are inert — LIP payout cron includes all MMs, weekly cron is no-op
lqf.mma_default_cap_usdt_weekly50000Default weekly cap ($USDT) when granting new MMA rows
lqf.mma_cron_hour_utc / lqf.mma_cron_minute0 / 20Weekly cron fires Monday UTC 00:20 (5 min after LIP daily at 00:15)
lqf.public_disclosure_endpointtruePublic LQF endpoints return 503 when off (emergency)

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 feeSplitPoller3-5 bps by tier
FEE_EXECUTION_BUILDERplatform:holdbuilder_exec:<user_id>CLOB feeSplitPoller3-5 bps by tier
FEE_OPERATORplatform:holdoperator:<user_id>CLOB feeSplitPoller1-2 bps (usually PAX = op_pax_v3)
FEE_MARKET_CREATORplatform:holdcreator:<user_id>CLOB feeSplitPoller2-3 bps
FEE_MM_STIPENDplatform:holdpool:mm_stipend:<market_id>CLOB feeSplitPoller4-5 bps; distributed daily via marketStipendDistributionCron using LQF quality score
FEE_LPplatform:holdpool:lpCLOB feeSplitPoller1-2 bps; distributed hourly via lpFeeDistributionCron
FEE_PLATFORM_NETplatform:holdplatform:netCLOB feeSplitPoller2 bps + absorbed unfilled attribution
LQF_STIPENDpool:mm_stipend:<market_id>user:<user_id>marketStipendDistributionCron (daily 00:15 UTC)Pro-rata payout of the market's daily pool by user_share = market_score / Σ market_score
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