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 ledger — you can independently verify at /verify, and live per-market config is queryable at the public LQF API.
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) | 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 split | At fill | FEE_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 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) |
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.
PAF_BASE_BPS = 20 — center peak at 50/50 (mathematical max of the raw curve)PAF_FLOOR_BPS = 8 — extreme-price floor (sustains revenue on directional flow near 0¢ / 100¢)PAF_CEILING_BPS = 25 — configurable safety cap; not reached with default PAF_BASE=20 (only active if PAF_BASE is raised in a future tuning)| Price | Raw curve | Effective (after clamp) |
|---|---|---|
| 0.50 | 20.00 bps | 20 bps |
| 0.70 | 16.80 bps | 16.80 bps |
| 0.90 | 7.20 bps | 8 bps (floor) |
| 0.99 | 0.79 bps | 8 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.
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 group | acq | exec | op | mc | mm_stipend | lp | plat_net | Total @ centre |
|---|---|---|---|---|---|---|---|---|
| Auto (self_serve / read_live / trade_capped) | 3 | 3 | 2 | 3 | 5 | 2 | 2 | 20 bps |
trade_full (admin) | 4 | 4 | 1 | 3 | 5 | 1 | 2 | 20 bps |
genesis / partner (admin) | 5 | 5 | 1 | 2 | 4 | 1 | 2 | 20 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.
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)
base_rate: FAST_MARKET_FEE_RATE = 0.05 (5%).0.05 × 0.5 × 0.5 = 1.25%); drops symmetrically to 0 at 0¢/100¢. No pool-imbalance surcharge, no winner-streak surcharge, no separate spread — the single p × (1 − p) curve is the entire model.lp_platform_share, lp_holder_share, lp_insurance_share) — unchanged from earlier iterations.LEGACY_FAST_MARKET_EDGE=on routes computeDynamicHouseEdge() back to the pre-Option-A dynamic 0.5–8% edge (retained for audit + rollback only; not the live formula).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 (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:
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.
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.
// 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.
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.
liquidityQualitySnapshotCron writes proximity-weighted rows to mm_quality_snapshots. Leader-only (Redis SET NX EX 45).mmStipendPoolAccumulatorCron sweeps FEE_MM_STIPEND events into mm_stipend_pool_ledger.accumulated_usdt.marketStipendDistributionCron computes user_share × pool → balanceManager.credit + financial_event(event_type='LQF_STIPEND') + mm_stipend_payouts row (idempotent via UNIQUE (user_id, market_id, epoch_start_at)).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.
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.
boost_multiplierNew 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.
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.
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.
Five no-auth endpoints let any trader / MM / auditor query the current LQF state without an API key:
| Endpoint | Returns |
|---|---|
GET /v1/lqf/config | List all active LQF markets (paginated, ordered by daily_stipend_usdt desc) |
GET /v1/lqf/config/:market_id | Full per-market config + formulas + reference URL |
GET /v1/lqf/pool/:market_id | Current pool balance (accumulated_usdt / distributed_usdt / balance_usdt) + last epoch payout |
GET /v1/lqf/leaderboard?days=7 | Top 50 MMs by past-week payout across all markets |
GET /v1/lqf/mma-tier | Public 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.
platform_settings, 30 s hot-reload)| Key | Default | Behavior when off |
|---|---|---|
lqf.boost_enabled | false | Snapshot cron treats boost_multiplier as 1.0 regardless of DB value |
lqf.hourly_uptime_enforce | false | Payout cron uses v1 unique_minutes / 1440 continuity (all hours count) |
lqf.hourly_min_snapshots | 45 | Min snapshots per hour for it to count as "present" (out of ~60 expected) |
lqf.mma_enabled | false | MMA rows are inert — LIP payout cron includes all MMs, weekly cron is no-op |
lqf.mma_default_cap_usdt_weekly | 50000 | Default weekly cap ($USDT) when granting new MMA rows |
lqf.mma_cron_hour_utc / lqf.mma_cron_minute | 0 / 20 | Weekly cron fires Monday UTC 00:20 (5 min after LIP daily at 00:15) |
lqf.public_disclosure_endpoint | true | Public LQF endpoints return 503 when off (emergency) |
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 | 3-5 bps by tier |
FEE_EXECUTION_BUILDER | platform:hold | builder_exec:<user_id> | CLOB feeSplitPoller | 3-5 bps by tier |
FEE_OPERATOR | platform:hold | operator:<user_id> | CLOB feeSplitPoller | 1-2 bps (usually PAX = op_pax_v3) |
FEE_MARKET_CREATOR | platform:hold | creator:<user_id> | CLOB feeSplitPoller | 2-3 bps |
FEE_MM_STIPEND | platform:hold | pool:mm_stipend:<market_id> | CLOB feeSplitPoller | 4-5 bps; distributed daily via marketStipendDistributionCron using LQF quality score |
FEE_LP | platform:hold | pool:lp | CLOB feeSplitPoller | 1-2 bps; distributed hourly via lpFeeDistributionCron |
FEE_PLATFORM_NET | platform:hold | platform:net | CLOB feeSplitPoller | 2 bps + absorbed unfilled attribution |
LQF_STIPEND | pool: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_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