Skip to content

Brokers & Referrals

A broker is a registered referral channel identified by a uint24 id. If you are routing trades on behalf of users — a frontend, a bot, a copy-trading service — a broker id lets you earn from the flow you bring.

Two independent mechanisms hang off that id:

Fee shareextraFee
Where the money comes fromA slice of the protocol fee the trade already paysAn extra charge on top
Effect on the traderNone — the trader pays the same either wayIncreases their cost
AmountFixed rate configured per brokerYou choose it, per trade
PayoutAccrues onchain, withdraw when you wantTransferred at fill
ConfiguredBy LeverUp, when your id is registeredThe amount is not — you pass it per call. The receiver still comes from your registered record

You can use either, both, or neither.

Getting an id

Broker registration is permissioned — you cannot self-register. Contact LeverUp to be assigned an id and to set the receiver address and commission rate. Once registered you can read your record back onchain at any time (see Reading your broker record).

Fee share

Pass your id in the broker field and a slice of that trade's fee is credited to your broker account:

commission = tradeFee × commissionP / 1e4

commissionP is set per broker by LeverUp — confirm your rate when your id is assigned. The split is bounded so that commission plus the protocol's own shares never exceeds the fee itself, which is why this costs the trader nothing. Passing a broker id does not change the fee they pay; it only changes where part of it goes.

Commission accrues per token in protocol storage rather than being transferred on each trade.

broker: 0 is not "no broker"

An id of 0, or an id that has never been registered, falls back to the protocol's default broker — it does not skip the referral. If you are not running a broker integration this is harmless and 0 is the right value to pass. But if you are, passing 0 by accident silently credits your flow to someone else.

This fallback covers the fee share only. extraFee has no fallback and behaves differently on an unregistered id.

Open and close are credited separately

The broker on a position's opening trade does not carry over to its close. The close fee is credited to whichever broker id is passed on the closing call — and to the id stored on a TP/SL order when a keeper executes one.

So a complete broker integration passes its id in four places:

  1. The open (OpenDataInput.broker)
  2. The close (closeTrade(hash, broker) / batchCloseTrade(hashes, broker) / partial close)
  3. Each TP/SL order it creates (DecreaseOrderInput.broker)
  4. Each TP/SL order it updates (DecreaseOrderUpdateInput.broker)

Miss one and that portion of the flow is credited to the default broker.

extraFee

extraFee is a surcharge you add on top of the trade, transferred to your broker's receiver address when the position opens.

The amount is not configured anywhere — the protocol stores no rate and enforces no cap, and it is simply a number your client passes on each open. The receiver is not yours to choose: it is the address on your registered broker record. extraFee only reaches you if you hold a registered id, and unlike the fee share it does not fall back to the default broker when the id is unknown.

solidity
// on the open — the surcharge is pulled and parked against the order
tokenIn.transferFrom(trader, extraFee);   // charged in addition to amountIn
pendingExtraFees[tradeHash] = extraFee;

// at fill — read straight off the broker record, with no _getBrokerOrDefault
address receiver = brokers[broker].receiver;
lvToken.transfer(receiver, convertedExtraFee);   // reverts when receiver is 0x0
Denominated intokenIn when you pass it; converted to lvToken at payout
Available onMarket open and limit open only
RefundedYes — when the open is refunded through the normal path, a failed check, extraFee comes back with the collateral. A reverted payout is not that path; see below
EventExtraFeeCharged(brokerId, token, amount, user, tradeHash)

What each broker value does to extraFee

brokerOutcome
Your registered idThe surcharge reaches your receiver.
0, or any other registered idThe open succeeds and the surcharge goes to that broker's receiver. The trader is charged and you receive nothing.
An unregistered idreceiver is the zero address and the transfer reverts. The position never opens.

It fails late, and quietly

Nothing on the open path validates the broker id. The surcharge is pulled from the trader in the submitting transaction and only paid out later, in the keeper's price callback — and that callback is invoked inside try … catch in LibPriceFacade, so a revert there is swallowed.

With an unregistered id and extraFee > 0, what you observe is not an error. It is an open that silently never fills, after the trader's extraFee has already left their wallet. Verify your id with getBrokerById before you send a non-zero extraFee.

Allowance must cover it

The trader's approval to the Diamond has to cover amountIn + extraFee — and on the gasless path, amountIn + extraFee + antiDdosFee when the execution fee is charged in the same token. A short allowance fails the transfer.

Because extraFee is a real cost to the trader, disclose it in your interface. It is not part of the protocol's quoted fees and will not appear in any fee preview you read from the contract.

Where broker appears

Both integration paths carry it, on every operation that charges a fee.

OperationOnchainGasless actionbrokerextraFee
Market openopenMarketTradeV2MARKET_OPEN (0)yesyes
Limit openopenLimitOrderV2LIMIT_OPEN (2)yesyes
ClosecloseTradeMARKET_CLOSE (1)yes
Batch closebatchCloseTradeBATCH_MARKET_CLOSE (8)yes
Partial closecloseTrade(hash, qty, broker)PARTIAL_CLOSE (9)yes
Create TP/SL ordersbatchCreateDecreaseOrdersBATCH_CREATE_DECREASE_ORDERS (11)per order
Update TP/SL ordersbatchUpdateDecreaseOrdersBATCH_UPDATE_DECREASE_ORDERS (12)per order
Cancel order, change TP/SL, add/remove marginactions 3–7, 10, 13

The last row has no broker field because those operations charge no fee — there is nothing to share.

Reading your broker record

solidity
function getBrokerById(uint24 id) external view returns (BrokerInfo memory);
function brokers(uint start, uint8 length) external view returns (BrokerInfo[] memory);
solidity
struct BrokerInfo {
    string name;
    string url;
    address receiver;          // where commission and extraFee are paid
    uint24 id;
    uint16 commissionP;        // 1e4 — your share of the trade fee
    uint16 daoShareP;          // 1e4
    uint16 LpPoolP;            // 1e4
    CommissionInfo[] commissions;
}

struct CommissionInfo {
    address token;
    uint total;                // lifetime
    uint pending;              // withdrawable now
}
ts
import { publicClient, DIAMOND } from './config'

const BROKER_ABI = [{
  type: 'function',
  name: 'getBrokerById',
  stateMutability: 'view',
  inputs: [{ name: 'id', type: 'uint24' }],
  outputs: [{
    type: 'tuple', name: '', components: [
      { name: 'name', type: 'string' },
      { name: 'url', type: 'string' },
      { name: 'receiver', type: 'address' },
      { name: 'id', type: 'uint24' },
      { name: 'commissionP', type: 'uint16' },
      { name: 'daoShareP', type: 'uint16' },
      { name: 'LpPoolP', type: 'uint16' },
      { name: 'commissions', type: 'tuple[]', components: [
        { name: 'token', type: 'address' },
        { name: 'total', type: 'uint256' },
        { name: 'pending', type: 'uint256' },
      ]},
    ],
  }],
}] as const

const broker = await publicClient.readContract({
  address: DIAMOND,
  abi: BROKER_ABI,
  functionName: 'getBrokerById',
  args: [myBrokerId],
})

console.log('rate     ', Number(broker.commissionP) / 100, '%')
console.log('receiver ', broker.receiver)

for (const c of broker.commissions) {
  console.log(`  ${c.token}  pending ${c.pending}  lifetime ${c.total}`)
}

commissions amounts use each token's own decimals — see Precision & Units.

Withdrawing commission

solidity
function withdrawCommission(uint24 id) external;

Sweeps every token with a non-zero pending balance for that broker in one call.

ts
const WITHDRAW_ABI = [{
  type: 'function',
  name: 'withdrawCommission',
  stateMutability: 'nonpayable',
  inputs: [{ name: 'id', type: 'uint24' }],
  outputs: [],
}] as const

const hash = await walletClient.writeContract({
  address: DIAMOND,
  abi: WITHDRAW_ABI,
  functionName: 'withdrawCommission',
  args: [myBrokerId],
})
await publicClient.waitForTransactionReceipt({ hash })

The call is permissionless — anyone can trigger it — but funds always go to the broker's registered receiver, so there is nothing to protect. You can have a keeper call it on a schedule without holding any privileged key. Emits WithdrawBrokerCommission(id, token, operator, amount).

To change the receiver address, contact LeverUp — it is an admin-controlled field.

Trading perpetuals involves risk. Nothing here is financial advice.