# 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](#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.

::: warning `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`](#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](/onchain/tpsl-orders) 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 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. |

::: danger 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`](#reading-your-broker-record) before you send a non-zero `extraFee`.
:::

::: warning Allowance must cover it
The trader's approval to the Diamond has to cover `amountIn + extraFee` — and on the
[gasless path](/gasless/overview), `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

```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](/introduction/precision).

## 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.
