Core concepts

Fees

Every fee on a trade or withdrawal, its unit, when it is charged, and where to read it

The quote is the authoritative number for what a trade costs. Read feeBreakdown and totalCostIncFees before the user commits (see Quotes & smart routing). After the trade, GET /execution/orders reports fees.quoted (the quote-time snapshot) and fees.actual for reconciliation.

Fee kinds

FeeUnitChargedQuote field
Venue trading feeVenue rate per marketWhen the order fills on the venuefeeBreakdown.venueFees, fills[].venueFee
Venue settlement feeShare of payout or profitAt settlement, on venues that charge this wayNetted from estimatedPayout, fills[].settlementFee
Builder feeUSDWith the order, on venues that use builder codesfeeBreakdown.builderFee
App fee (yours)Basis points of routed notionalBuys, and sells when you set a sell rate. See App feefeeBreakdown.appFee, appFeeBips
Referral feeBasis points of routed notionalBuys, and sells when you set a sell rate. See Referral feefeeBreakdown.referralFee, referral
Bridge feeUSDWhen funds move across chains for the tradefeeBreakdown.bridgeFees
Execution gasUSDNetwork gas for executionfeeBreakdown.executionGas
Setup costsUSDOne time, per wallet and chainfeeBreakdown.setupCosts with deepEstimate=true. Hyperliquid activation (about $1.23 when bridging to a new HyperCore address: Hyperliquid's 1 USDC plus Relay's ~$0.20; budgeted as $1.25) is counted on every quote that routes to Hyperliquid, with or without deepEstimate
Withdrawal feeToken atomic unitsOn withdrawalfeeRaw on the withdrawal preview

feeBreakdown.totalCost is price times shares plus venue, builder, bridge, and gas fees. It excludes app and referral fees. totalCostIncFees includes everything.

Venue fees

Each venue sets its own taker fee. GET /venue-markets returns it per market as feeRate, feeExponent, and feeBasis (fill, fill_in_game_only, winnings, or exit). null means sufwan-local-test does not have the fee, not that it is free. Use these fields to render fee-inclusive price levels, and use the quote for the real cost. Formulas and a per-venue table: Venue fees.

Builder fee

Polymarket and Hyperliquid attribute order flow to a builder. Set your own builder code per venue in the admin dashboard. Without one, the platform default applies. See Builder codes.

App fee

Your app fee is the fee you charge your users. Configure rates per app in the admin dashboard under Fees. It applies to buys, in basis points of the routed notional. Sells are free unless you set a sell rate (Fallback sell fee) under Fees; a sell is then charged on gross proceeds (shares × price, before the venue fee) and the fee comes out of the proceeds. The quote shows it in feeBreakdown.appFee and nets it out of estimatedNetProceeds, which is gross proceeds minus venue fees, builder fees, your app fee and any user-paid referral.

Fee recipient wallets. Set where your share is paid under Fees too: one EVM address (it covers every EVM chain), a Solana address, and a HyperCore address. Each venue pays out on the chain it settles on, so a trade whose fee settles where you have no wallet pays you nothing. On a self-custody trade it is refused instead (no_fee_recipient); see Self-custody trading.

Sell fees are collected on Polymarket, Hyperliquid, predict.fun, Limitless, Myriad, Novig and ProphetX. A sell leg on any other venue carries no fee, with a sell_fee_unsupported_venue warning on the quote. A sell that fills on more than one venue carries no app fee or user-paid referral, with a sell_fee_multi_venue warning. A managed Polymarket sell of shares held outside the user's deposit wallet (a legacy Safe or proxy) carries no app fee or user-paid referral, with a sell_fee_unsupported_wallet warning. A managed Novig or ProphetX sell settles on Solana, where a user-paid referral cannot be paid: the app fee still applies, but the referral is priced at 0 (and not deducted from estimatedNetProceeds), with a sell_referral_unsupported_venue warning. An app-paid referral is unaffected. Warnings use the usual { venue, venueMarketOutcomeId, reason } shape.

On a managed buy the quote reserves the app fee out of maxSpend. Partial fill: the app fee is collected on the amount actually filled. feeBreakdown.appFee is the maximum if the buy fills in full.

Where a managed buy's fee comes from. The fee is collected after the fill, from the user's managed wallet, on the chain of the venue that filled: Polygon for Polymarket, BNB Chain for predict.fun. You don't need to keep funds on that chain. When a buy is funded from another chain, the bridge carries the reserved fee along with the trade, and the fee is collected where it lands:

  • From the managed wallet on the venue's chain, in the stablecoin that is there (for example USDT on BNB Chain). This is used whenever that wallet already holds the whole fee, and it confirms within seconds.
  • From the Polymarket deposit wallet, in pUSD, when the buy's funds went there. This transfer can only be sent once the fill has settled on-chain, so the fee usually confirms within a minute. Until then the charge reads failed with a "waits for on-chain settlement" reason, and it is retried automatically.

fees.actual.appFee.status reads under_collected when less than the full fee was collected, including when nothing was (collectedRaw: "0").

  • A partial collection because the user's wallet was short is final.
  • On Hyperliquid the partner and platform shares go out as two separate transfers. If one has not gone out yet, a retry sends it and the fee moves to confirmed.
  • A charge that collected nothing is retried automatically, up to 5 times over about 2.5 hours, and is final after that.

fees.actual.appFee.finalizedAt is the time the fee stopped changing: collected in full, skipped, or no longer retried. It is absent (never null) while the fee may still change, so a $0 under_collected without finalizedAt is still being retried.

Per-trade override. A request that carries your x-app-api-key can replace the rate for one trade with appFeeBips (0 to 10000; 0 waives it) on the quote or a direct order. The quote then reads feeBreakdown.appFeeCategory: "override". Without an API key the request returns 400 with a message starting app_fee_override_requires_api_key. The override works on sells too. The platform share applies to an overridden fee the same way.

Venue-specific rates. To charge a different rate per venue, send appFeeBipsByVenue on the quote or a direct order, with the same x-app-api-key rule. Keys are venue names and each value is 0 to 10000 bips. A venue's rate is resolved in this order:

  1. appFeeBipsByVenue[venue]
  2. appFeeBips
  3. your configured rate for the category, then all

So appFeeBipsByVenue: { "polymarket": 20, "hyperliquid": 10 } charges 20 bips on the legs that fill on Polymarket and 10 on the legs that fill on Hyperliquid. A 0 in the map waives the fee on that venue even when appFeeBips is set. On a quote:

  • feeBreakdown.appFee charges each venue's share of the notional at its own rate.
  • feeBreakdown.appFeeBips is the highest rate among the venues that route fills.
  • feeBreakdown.appFeeBipsByVenue lists the rate for each venue the route fills. It is present only when those rates differ.
  • maxSpend is reserved at the highest rate among all candidate venues, since the fill venues are not known until the route is built. A venue with a lower rate therefore gets a slightly smaller budget, and maxSpend is never exceeded.

A direct order fills on one venue, so its rate is the map entry for that venue. On a quote, the map is a JSON string: a malformed map, an unknown venue or a rate that is not a whole number in 0..10000 returns 400 with a message starting app_fee_override_invalid. On POST /execution/orders the map is an object: an unknown venue returns the same app_fee_override_invalid, but a rate that is not a whole number in 0..10000 fails request validation first (400, Request validation failed).

A common split is a fixed total shared with a referrer: appFeeBips: 20 with no referrer, or appFeeBips: 10 with referrerFeeBips: 10 when there is one. The user pays 20 bips either way.

Self-custody fills collect the app fee differently, and under approveMode: "user_broadcast" the fee accrues as a balance you claim at the bridge provider. See Self-custody trading.

Referral fee

A referral pays a third party out of the same trade. It is a second fee next to the app fee, in basis points of the routed notional, and it goes entirely to referrer. sufwan-local-test takes no share of it.

Send these on the quote or on POST /execution/orders. They need x-app-api-key. Without it the request returns 400 with a message starting referral_requires_api_key. The fill uses the quote's referral and cannot change it.

ParamRequiredUse
referrerEVM address that receives the fee. Works on sells too, on the same venues as the app fee.
referrerFeeBipsWith referrer1 to 10000.
referrerFeeBipsByVenuePer-venue referral rates, 0 to 10000, same precedence as appFeeBipsByVenue. 0 means no referral on that venue; at least one venue must end up above 0. A route or order that fills only venues at 0 carries no referral. With it, referrerFeeBips is optional.
referralPayer"user" (default) or "app".
referralPayerAddressWith "app"Your wallet that pays the referral.
referralPayoutChainIdApp-paid only. The chain the referrer is paid on. Must carry USDC.

These are HTTP fields. getSmartRoute forwards referrer, referrerFeeBips, referralPayer, and referralPayerAddress. It does not forward referralPayoutChainId.

The quote echoes the accepted referral as referral: { referrer, feeBips, payer, payerAddress?, quotedFeeUsd }. A user-paid referral also appears in feeBreakdown.referralFee. An app-paid referral is not a user cost and never does.

User-paid

Partial fill: a user-paid referral is reserved from maxSpend and collected the same way as the app fee.

App-paid

The user pays nothing. You pay the referrer from referralPayerAddress. Partial fill: an app-paid referral pays the quoted amount. A failed fill pays nothing.

Without referralPayoutChainId, the payout goes on the trade's fee chain, or Polygon when that chain is not EVM. Sending referralPayoutChainId with a user-paid referral returns 400 with referral_invalid.

On HyperCore the referrer receives spot USDC on Hyperliquid. The first payout to a referrer with no Hyperliquid account costs an extra 1 USDC that Hyperliquid charges to create it. Your wallet must hold the payout plus that 1 USDC, or the transfer is refused.

Poll and sign that payout on Referrals. Signature types are on Self-custody signing.

Where a referral cannot be paid

A buy that fills only on Kalshi collects its fee from Solana USDC, where an EVM referrer cannot be paid. The quote carries a warning with reason: "referral_unpaid_on_solana", the user is not charged the referral, and nothing is paid.

A user-paid referral on a Hyperliquid fill is sent to the referrer on HyperCore. Hyperliquid charges the sender 1 USDC to activate an address that has never received a HyperCore deposit. If the referrer's address is not activated, the referral transfer either costs the user that 1 USDC or, if they can't cover it, is rejected and the referral is not paid. The quote carries a warning with reason: "referrer_not_activated_on_hypercore", and the trade still executes. Fund the referrer's HyperCore address once, with any amount, to clear it.

To pay that activation fee yourself instead, send referralActivationSponsorAddress on the quote (or the direct order). Before the fill, a pending signature appears beside the order's with signerAddress set to that wallet, purpose: "hl_activation_sponsor" and optional: true: an eip712 sendAsset of 0.000001 USDC to the referrer. Sign it with that wallet and post it on POST /execution/fill/:quoteId/signatures together with the order's signature (with the SDK, your signer callback receives it). After the fill it is sent before the referral transfer, only if the referrer still has no HyperCore account. The wallet's HyperCore spot balance must hold at least 1.000001 USDC. Declining it is safe: the trade proceeds and the referral is attempted as it would be without it. Self-custody Hyperliquid buys only, user-paid referral only.

If you drive signatures with your own client rather than the SDK, post the required and optional signatures together, but never wait more than about 15 seconds on your paying wallet's signature (or any optional request signed by a wallet other than the user's): post without it after that. The order request expires, so the order's signature must not be held for it. An optional request the user's own wallet signs, such as an hl_referral prompt, can wait for the user as before.

Securing the paying wallet's signer

The activation request reaches your backend through the user's browser, so the endpoint that signs it is a signing service any user can call with whatever typed data they like, for example a sendAsset of the paying wallet's whole balance to themselves, which they could post to Hyperliquid directly. Sign only when every one of these holds, and otherwise refuse:

  • primaryType is HyperliquidTransaction:SendAsset.
  • destination equals the referrer you set on that quote yourself (compare case-insensitively).
  • amount is exactly "0.000001".
  • token is exactly "USDC:0x6d1e7cde53ba9467b783cb7c530ce054", Hyperliquid's SYMBOL:tokenId wire form for spot USDC.
  • sourceDex and destinationDex are both "spot", and fromSubAccount is "".

Better still, rebuild the typed data on your server from the quote you created and sign that, rather than trusting the client's copy. The paying wallet must be an EOA holding the master key: Hyperliquid agent (API) wallets cannot sign sendAsset, and a smart-contract wallet's signature fails verification, so the activation payment is skipped.

Reconciling

GET /execution/orders reports fees.quoted.referralFeeRaw and fees.actual.referral per trade: { referrer, payer, feeBips, dueRaw, paidRaw, status }, where status is confirmed, failed, expired, or skipped.

Withdrawal fees

POST /execution/withdraw/preview returns the fee as feeRaw and the amount the recipient gets as receiveAmountRaw. Fees come out of the withdrawal amount. See Funding & withdrawals.

On this page