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 share | extraFee | |
|---|---|---|
| Where the money comes from | A slice of the protocol fee the trade already pays | An extra charge on top |
| Effect on the trader | None — the trader pays the same either way | Increases their cost |
| Amount | Fixed rate configured per broker | You choose it, per trade |
| Payout | Accrues onchain, withdraw when you want | Transferred at fill |
| Configured | By LeverUp, when your id is registered | The 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 / 1e4commissionP 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:
- The open (
OpenDataInput.broker) - The close (
closeTrade(hash, broker)/batchCloseTrade(hashes, broker)/ partial close) - Each TP/SL order it creates (
DecreaseOrderInput.broker) - 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.
// 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 in | tokenIn when you pass it; converted to lvToken at payout |
| Available on | Market open and limit open only |
| Refunded | Yes — 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 |
| Event | ExtraFeeCharged(brokerId, token, amount, user, tradeHash) |
What each broker value does to extraFee
broker | Outcome |
|---|---|
| Your registered id | The surcharge reaches your receiver. |
0, or any other registered id | The open succeeds and the surcharge goes to that broker's receiver. The trader is charged and you receive nothing. |
| An unregistered id | receiver 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.
| Operation | Onchain | Gasless action | broker | extraFee |
|---|---|---|---|---|
| Market open | openMarketTradeV2 | MARKET_OPEN (0) | yes | yes |
| Limit open | openLimitOrderV2 | LIMIT_OPEN (2) | yes | yes |
| Close | closeTrade | MARKET_CLOSE (1) | yes | — |
| Batch close | batchCloseTrade | BATCH_MARKET_CLOSE (8) | yes | — |
| Partial close | closeTrade(hash, qty, broker) | PARTIAL_CLOSE (9) | yes | — |
| Create TP/SL orders | batchCreateDecreaseOrders | BATCH_CREATE_DECREASE_ORDERS (11) | per order | — |
| Update TP/SL orders | batchUpdateDecreaseOrders | BATCH_UPDATE_DECREASE_ORDERS (12) | per order | — |
| Cancel order, change TP/SL, add/remove margin | actions 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
function getBrokerById(uint24 id) external view returns (BrokerInfo memory);
function brokers(uint start, uint8 length) external view returns (BrokerInfo[] memory);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
}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
function withdrawCommission(uint24 id) external;Sweeps every token with a non-zero pending balance for that broker in one call.
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.