Problem Statement
Traders placing orders in HyperTerminal see fee information that is wrong or confusing in three distinct ways:
- Wrong numbers on HIP-3 markets. The
Est. Fee row shows the validator-dex rate even when the user is trading a builder-deployed perpetual (e.g. brentoil). HIP-3 markets apply a multiplier of 2 × deployerFeeScale × (growthMode ? 0.1 : 1) on top of the user's validator rate. Subsidized (growth-mode) markets can be ~5× cheaper than what we currently display, while non-subsidized HIP-3 markets are up to 2× more expensive. Users see the wrong number either way.
- No account for the user's actual fee tier. The
userFees endpoint already returns the user's personalized userCrossRate / userAddRate (reflecting VIP tier, staking discount, and referral discount), but when that fetch hasn't resolved we fall back to hardcoded ORDER_FEE_RATE_* constants. A user who would actually pay 0.0086% may briefly see 0.0450%.
- Builder fee is unexplained. The row reads
Builder Fee 0.01%. Users ask "0.01% of what?" — they cannot tell whether it is a percentage of the exchange fee, of the order value, or something else. The value proposition (who this goes to, why it exists) is invisible.
Collectively, these make the fee area feel opaque. Traders do not trust the displayed numbers, and those who look closely find they are sometimes provably wrong on HIP-3 markets.
Solution
Replace the current two-row fee block with a single Fee row that shows one honest, API-sourced total, with a hover/tap tooltip that reveals the breakdown.
From the user's perspective:
- Only one number on the trade summary: the total they will pay, both as a percentage and as a dollar amount.
- On hover (desktop) or tap (mobile), a compact tooltip shows: the exchange fee (taker / maker), a strikethrough tier-0 reference rate when their effective rate is discounted or subsidized, the builder fee with a single-line explanation, and the grand total.
- HIP-3 markets like
brentoil display their actual effective rate — the same number a user would see in Hyperliquid's own UI. Subsidized markets show the strikethrough so the saving is visible.
- When the wallet is disconnected, the Fee row is hidden entirely — personalized rates require an address, and showing generic tier-0 numbers would be misleading.
User Stories
- As a connected trader on BTC-PERP, I want to see my actual fee (including my VIP tier and staking discount), so that I know what I will pay before I click Buy.
- As a connected trader on a HIP-3 subsidized market like
brentoil, I want to see the subsidized rate (not the validator rate), so that I do not overestimate my costs and skip a profitable trade.
- As a connected trader on a HIP-3 non-subsidized market, I want to see the higher rate I will actually pay, so that I am not surprised by a larger-than-expected fee.
- As a trader on any HIP-3 market, I want to see the tier-0 rate struck through next to my effective rate, so that I can visually confirm I am getting a subsidy or discount.
- As a trader placing a limit order, I want the tooltip to show both taker and maker rates, so that I can reason about which side of the book I want to post on.
- As a trader placing a market order, I want the row to show the taker number (not maker), so that the displayed amount matches what I will actually be charged.
- As a curious user, I want to hover over the Fee row and see a breakdown, so that I understand how the total was computed.
- As a user on a mobile device, I want to tap the Fee row to toggle the tooltip open, so that I can read the breakdown without a hover event.
- As a user reading the tooltip, I want a one-line explanation of the builder fee, so that I understand what I am paying for without having to read a paragraph.
- As a user who is not connected, I want the Fee row to be hidden, so that I am not shown a generic rate that would not apply to me.
- As a user whose
userFees / perpDexs / meta responses are still loading, I want the Fee row to render a skeleton, so that I do not briefly see a stale or validator-only number before it corrects.
- As a user whose effective rate equals the tier-0 rate (no discount, no subsidy), I want the strikethrough line hidden, so that the tooltip does not lie to me by striking through an identical number.
- As a user trading a spot market, I want the tooltip to use the spot base rates as the strikethrough baseline, so that the comparison is apples-to-apples.
- As a user trading a builder-perp market in growth mode, I want a "Subsidized market" hint in the tooltip, so that I understand why the rate is much lower than tier-0.
- As a developer maintaining the fee code, I want HIP-3 math extracted into a pure function, so that I can write unit tests that assert the formula directly without needing React, the DOM, or network mocks.
- As a developer debugging a fee mismatch vs. Hyperliquid's own UI, I want the fee pipeline to be a single pure function that takes well-typed inputs, so that I can reproduce the calculation in a test instead of scrubbing through a render tree.
- As a future contributor adding a new market type (e.g. spot HIP-3), I want the effective-fee function to accept a clear shape, so that I can add a new branch without ripping out the surrounding code.
- As a user who has approved a non-default builder fee for this address, I want the tooltip to show the actual builder fee in effect, so that the number matches what the exchange will charge.
- As a user on a build where the builder config has fee
0, I want the builder line suppressed entirely, so that the tooltip does not show an empty row.
- As a trader opening a partial-fill scenario, I want the total to reflect the current size × price at entry, so that the displayed dollar amount tracks what I am about to submit.
Implementation Decisions
Data sourcing (single source of truth: the Hyperliquid API).
- All fee inputs come from live API responses. The hardcoded
ORDER_FEE_RATE_* constants are removed; they are no longer used as fallbacks when the wallet is connected.
userFees provides userCrossRate, userAddRate, userSpotCrossRate, userSpotAddRate, and the full feeSchedule (including tier-0 rates used as strikethrough baseline).
perpDexs[dexIndex].deployerFeeScale provides the per-HIP-3-dex multiplier.
meta.universe[assetIndex].growthMode === "enabled" provides the per-asset growth-mode flag.
- A HIP-3 market is identified by
UnifiedMarket.kind === "builderPerp", which already exposes dex and dexIndex.
- The
MarketsProvider already fetches perpDexs and allPerpMetas; no new top-level fetches are required.
Deep module: effective-fee calculator.
- A new pure function computes the effective taker and maker rates. Inputs: the user's base rate (taker and maker), tier-0 base rate (for strikethrough), market kind, a flag indicating HIP-3,
deployerFeeScale (when HIP-3), and growthMode (when HIP-3). Output: { effective, base, isDiscounted, isSubsidized }.
- Formula:
effective = userRate × 2 × deployerFeeScale × (growthMode ? 0.1 : 1) on HIP-3 markets; effective = userRate on validator markets. Multiplication is performed with big.js on string inputs; the output rates are returned as strings and converted to number only at the display seam.
- The function has no React, no data-fetching, no DOM dependency. It is a pure transformation from typed inputs to typed outputs.
Orchestrator: useFeeRates rewrite.
- Signature changes from
useFeeRates(marketKind?) to useFeeRates(market) where market is the current UnifiedMarket (or undefined).
- Pulls
userFees, perpDexs, and allPerpMetas from existing providers (MarketsProvider, useInfo("userFees")).
- Returns
null when the wallet is disconnected or when any of userFees / perpDexs / allPerpMetas is still loading.
- Returns
{ effective: { taker, maker }, base: { taker, maker }, flags: { isHip3, isGrowthMode, isDiscounted, isSubsidized }, builderBps } otherwise.
- The hook is thin: it composes the effective-fee calculator with data fetching. It does not contain fee math itself.
Updated order-metrics function.
getOrderMetrics accepts { sizeValue, price, leverage, exchangeRate, builderBps } and returns { orderValue, marginRequired, exchangeFee, builderFee, totalFee }.
- Remains a pure function; existing tests in
domain/trade/ continue to cover the non-fee outputs.
UI: single Fee row + tooltip.
OrderSummary renders one row labeled Fee, value <effective%> (<$ amount>). The label is underline-dotted to signal the tooltip affordance (matches the existing pattern on the trade-details "Fees" row).
- When
useFeeRates returns null, the row is not rendered at all (wallet disconnected) or renders a skeleton (still loading).
- A new
FeeTooltip component renders the breakdown:
Exchange fee <taker%> / <maker%>
- strikethrough tier-0 row, shown only when
isDiscounted || isSubsidized
Builder fee <bps%> ($<amount>) with a single short line: "Supports interface development"
- divider +
Total (taker) <total%> ($<amount>)
- A small "Subsidized market" hint when
isHip3 && isGrowthMode
- The tooltip uses
@hypeterminal/ui's existing <Tooltip>, which handles hover on desktop and tap on mobile.
Verification gate before merge.
- Implementer opens Hyperliquid's own frontend for (a) a validator market, (b) a HIP-3 growth-mode market, (c) a HIP-3 non-growth market. Our tooltip's "Your rate" must match Hyperliquid's displayed rate to four decimals for the connected wallet. If a mismatch is found, the formula is wrong and the PR does not merge.
Testing Decisions
What makes a good test here: tests assert external behavior of the pure effective-fee function. Given a concrete set of inputs (user rate, tier-0 rate, dex scale, growth flag), the output rates are deterministic. Tests do not mount components, do not mock hooks, and do not touch the DOM.
Covered by tests.
- The effective-fee pure function. Cases: (1) validator market with zero discount, (2) validator market with staking + referral + VIP discount, (3) HIP-3 market with
deployerFeeScale = 1 and no growth mode, (4) HIP-3 market with deployerFeeScale = 1 and growth mode on, (5) HIP-3 market with deployerFeeScale > 1, (6) spot validator market, (7) string-precision cases where floating-point would drift (e.g. very small rates near 1e-6), (8) edge case where the user's base rate is "0".
- Prior art: existing Vitest tests in
apps/terminal/src/lib/tests/ and apps/terminal/src/domain/trade/; follow the same conventions.
Not covered by tests (deliberately).
useFeeRates hook — thin orchestration, churns often, mostly glue.
FeeTooltip and OrderSummary components — presentational, churn with design iterations, visual regressions are better caught by eye than by a test harness.
Out of Scope
- Tier ladder and progress-to-next-tier UI (e.g. "You are on Tier 2 — $18M volume to Tier 3"). That belongs on a dedicated Account > Fees page and is a separate PR.
- Displaying active staking-discount tier names ("Gold", "Diamond") in the trade tooltip.
- Referral-discount attribution in the tooltip (we show the net effective rate only; the user's referral status is already accounted for by the API).
- Redesigning the disconnected-wallet trade summary beyond "hide the Fee row".
- Any change to the builder approval flow (
approveBuilderFee, maxBuilderFee) or to the default builder address / fee in config/hyperliquid.ts.
- Spot HIP-3 markets — not yet a product; adding a branch for them is speculative.
- Localization / translation of the single-line builder fee hint. It inherits the existing lingui
t macro.
Further Notes
- The strikethrough baseline is the tier-0 validator rate (
0.0450% / 0.0150% perp; 0.0700% / 0.0400% spot). This matches the convention used by Hyperliquid's own UI and frames the display as "savings relative to a new account", which is the most intuitive frame for most users.
- The tooltip stays tight on purpose. Longer prose invites the very "what is this fee for?" confusion this PRD is trying to solve; minimal value-first rows are the cure.
- The verification step (comparing our rate against Hyperliquid's UI on three markets) is cheap and catches the formula being wrong at
×2 instead of ÷2, or any other sign-of-mistake reversal.
- This PRD will be broken into sub-issues once approved: (a) effective-fee module + tests, (b)
useFeeRates rewrite + constant removal, (c) OrderSummary + tooltip UI, (d) verification pass.
Problem Statement
Traders placing orders in HyperTerminal see fee information that is wrong or confusing in three distinct ways:
Est. Feerow shows the validator-dex rate even when the user is trading a builder-deployed perpetual (e.g.brentoil). HIP-3 markets apply a multiplier of2 × deployerFeeScale × (growthMode ? 0.1 : 1)on top of the user's validator rate. Subsidized (growth-mode) markets can be ~5× cheaper than what we currently display, while non-subsidized HIP-3 markets are up to 2× more expensive. Users see the wrong number either way.userFeesendpoint already returns the user's personalizeduserCrossRate/userAddRate(reflecting VIP tier, staking discount, and referral discount), but when that fetch hasn't resolved we fall back to hardcodedORDER_FEE_RATE_*constants. A user who would actually pay0.0086%may briefly see0.0450%.Builder Fee 0.01%. Users ask "0.01% of what?" — they cannot tell whether it is a percentage of the exchange fee, of the order value, or something else. The value proposition (who this goes to, why it exists) is invisible.Collectively, these make the fee area feel opaque. Traders do not trust the displayed numbers, and those who look closely find they are sometimes provably wrong on HIP-3 markets.
Solution
Replace the current two-row fee block with a single Fee row that shows one honest, API-sourced total, with a hover/tap tooltip that reveals the breakdown.
From the user's perspective:
brentoildisplay their actual effective rate — the same number a user would see in Hyperliquid's own UI. Subsidized markets show the strikethrough so the saving is visible.User Stories
brentoil, I want to see the subsidized rate (not the validator rate), so that I do not overestimate my costs and skip a profitable trade.userFees/perpDexs/metaresponses are still loading, I want the Fee row to render a skeleton, so that I do not briefly see a stale or validator-only number before it corrects.0, I want the builder line suppressed entirely, so that the tooltip does not show an empty row.Implementation Decisions
Data sourcing (single source of truth: the Hyperliquid API).
ORDER_FEE_RATE_*constants are removed; they are no longer used as fallbacks when the wallet is connected.userFeesprovidesuserCrossRate,userAddRate,userSpotCrossRate,userSpotAddRate, and the fullfeeSchedule(including tier-0 rates used as strikethrough baseline).perpDexs[dexIndex].deployerFeeScaleprovides the per-HIP-3-dex multiplier.meta.universe[assetIndex].growthMode === "enabled"provides the per-asset growth-mode flag.UnifiedMarket.kind === "builderPerp", which already exposesdexanddexIndex.MarketsProvideralready fetchesperpDexsandallPerpMetas; no new top-level fetches are required.Deep module: effective-fee calculator.
deployerFeeScale(when HIP-3), andgrowthMode(when HIP-3). Output:{ effective, base, isDiscounted, isSubsidized }.effective = userRate × 2 × deployerFeeScale × (growthMode ? 0.1 : 1)on HIP-3 markets;effective = userRateon validator markets. Multiplication is performed withbig.json string inputs; the output rates are returned as strings and converted tonumberonly at the display seam.Orchestrator:
useFeeRatesrewrite.useFeeRates(marketKind?)touseFeeRates(market)wheremarketis the currentUnifiedMarket(orundefined).userFees,perpDexs, andallPerpMetasfrom existing providers (MarketsProvider,useInfo("userFees")).nullwhen the wallet is disconnected or when any ofuserFees/perpDexs/allPerpMetasis still loading.{ effective: { taker, maker }, base: { taker, maker }, flags: { isHip3, isGrowthMode, isDiscounted, isSubsidized }, builderBps }otherwise.Updated order-metrics function.
getOrderMetricsaccepts{ sizeValue, price, leverage, exchangeRate, builderBps }and returns{ orderValue, marginRequired, exchangeFee, builderFee, totalFee }.domain/trade/continue to cover the non-fee outputs.UI: single Fee row + tooltip.
OrderSummaryrenders one row labeledFee, value<effective%> (<$ amount>). The label is underline-dotted to signal the tooltip affordance (matches the existing pattern on the trade-details "Fees" row).useFeeRatesreturnsnull, the row is not rendered at all (wallet disconnected) or renders a skeleton (still loading).FeeTooltipcomponent renders the breakdown:Exchange fee <taker%> / <maker%>isDiscounted || isSubsidizedBuilder fee <bps%> ($<amount>)with a single short line: "Supports interface development"Total (taker) <total%> ($<amount>)isHip3 && isGrowthMode@hypeterminal/ui's existing<Tooltip>, which handles hover on desktop and tap on mobile.Verification gate before merge.
Testing Decisions
What makes a good test here: tests assert external behavior of the pure effective-fee function. Given a concrete set of inputs (user rate, tier-0 rate, dex scale, growth flag), the output rates are deterministic. Tests do not mount components, do not mock hooks, and do not touch the DOM.
Covered by tests.
deployerFeeScale = 1and no growth mode, (4) HIP-3 market withdeployerFeeScale = 1and growth mode on, (5) HIP-3 market withdeployerFeeScale > 1, (6) spot validator market, (7) string-precision cases where floating-point would drift (e.g. very small rates near1e-6), (8) edge case where the user's base rate is"0".apps/terminal/src/lib/tests/andapps/terminal/src/domain/trade/; follow the same conventions.Not covered by tests (deliberately).
useFeeRateshook — thin orchestration, churns often, mostly glue.FeeTooltipandOrderSummarycomponents — presentational, churn with design iterations, visual regressions are better caught by eye than by a test harness.Out of Scope
approveBuilderFee,maxBuilderFee) or to the default builder address / fee inconfig/hyperliquid.ts.tmacro.Further Notes
0.0450% / 0.0150%perp;0.0700% / 0.0400%spot). This matches the convention used by Hyperliquid's own UI and frames the display as "savings relative to a new account", which is the most intuitive frame for most users.×2instead of÷2, or any other sign-of-mistake reversal.useFeeRatesrewrite + constant removal, (c)OrderSummary+ tooltip UI, (d) verification pass.