# Builder Codes

> Canonical: https://docs.velocity.exchange/developers/velocity-sdk/builder-codes

Builder Codes let integrators earn fees by routing order flow through their application. A builder fee rides on top of the taker's normal tiered fee: it attaches to an order through `builderIdx` and `builderFeeTenthBps`, and it settles to the builder's revenue-share account when the order fills. The program caps it at 1% of notional via `MAX_BUILDER_FEE_TENTH_BPS`.

Not every fill pays it. The fee is waived, and the fill still goes through at zero builder fee, on liquidation fills and on any fill where the taker does not clear initial margin under the program's strict oracle rules. Build accounting around fees that arrive, not fees that were expected.

For the concepts behind this system, the three actors, the escrow model, and the payout paths, see [Velocity Builder Codes](/developers/builder-codes.md). This page covers the SDK calls that implement it.

> **Info:**
>
> Builder codes work on **any** perp order: regular onchain order placement (`placePerpOrder`, `placeAndTakePerpOrder`, `placeAndMakePerpOrder`, `fillPerpOrder`) as well as [Swift](/developers/velocity-sdk/swift.md) signed-message orders. `builderIdx`/`builderFeeTenthBps` are plain optional fields on `OrderParams`, not a Swift-only mechanism.

The model has three actors and three setup steps:

1. **Builder** initializes a `RevenueShare` account: a one-time, per-authority setup that lets the builder accrue fee-share rewards.
2. **User** initializes a `RevenueShareEscrow` account: tracks the builders this user has approved and any pending (unsettled) builder-fee orders.
3. **User** approves the builder in their escrow with a maximum fee cap (`changeApprovedBuilder`).

Only after all three steps can the user's orders carry that builder's fee.

## Setup

Create and subscribe separate `VelocityClient` instances for the builder and user authorities:

  
```js
import { Connection, Keypair } from "@solana/web3.js";
import { VelocityClient, Wallet } from "@velocity-exchange/sdk";

const connection = new Connection("<RPC_URL>", "confirmed");

// Builder wallet (revenue share provider)
const builderKeypair = Keypair.fromSecretKey(/* builder secret key */);
const builderWallet = new Wallet(builderKeypair);
const builderAuthority = builderKeypair.publicKey;

// User wallet (end user)
const userKeypair = Keypair.fromSecretKey(/* user secret key */);
const userWallet = new Wallet(userKeypair);
const takerAuthority = userKeypair.publicKey;

// Builder client
const builderClient = new VelocityClient({
  connection,
  wallet: builderWallet,
  env: "mainnet-beta",
});
await builderClient.subscribe();

// User client
const userClient = new VelocityClient({
  connection,
  wallet: userWallet,
  env: "mainnet-beta",
});
await userClient.subscribe();
```
  

## SDK Usage

### Builder: Initialize Revenue Share

Create the builder's onchain `RevenueShare` configuration account (one per authority, not per subaccount). This must exist before the builder can receive any fees:

  
```js
await builderClient.initializeRevenueShare(builderAuthority);
```
  

### User: Initialize Escrow

Create the user's `RevenueShareEscrow` account used for builder-fee tracking and approvals. `numOrders` sizes the pending-order slot list (it can be grown later with `resizeRevenueShareEscrowOrders`, but never shrunk): size it to at least the number of concurrently open orders expected across all of this user's subaccounts:

  
```js
// numOrders should cover concurrent open orders across all of the user's subaccounts
await userClient.initializeRevenueShareEscrow(takerAuthority, 16);
```
  

On creation, the escrow's `referrer` field is copied from the user's existing `UserStats.referrer`, if any (see [Fill-time enforcement](#fill-time-enforcement) below).

### User: Approve a Builder (Max Fee)

Approve a builder and set the maximum fee they may charge. The `builderIdx` used on orders references the position of this approval in the user's `RevenueShareEscrow.approvedBuilders` list:

  
```js
// maxFeeTenthBps is in tenths of a basis point (10 = 1 bp = 0.01%)
await userClient.changeApprovedBuilder(builderAuthority, 200, true); // cap: 20 bps (0.2%)
```
  

Call with `add = false` to revoke a builder. That fails with `CannotRevokeBuilderWithOpenOrders` while the builder still has open, unsettled orders outstanding: cancel or settle those first.

### Order Placement (Builder Fee on a Regular Order)

Include `builderIdx` and `builderFeeTenthBps` directly on `OrderParams` for any perp order placement:

  
```js
import { MarketType, OrderType, PositionDirection } from "@velocity-exchange/sdk";

await userClient.placePerpOrder({
  orderType: OrderType.LIMIT,
  marketType: MarketType.PERP,
  marketIndex: 0,
  direction: PositionDirection.LONG,
  baseAssetAmount: userClient.convertToPerpPrecision(1),
  price: userClient.convertToPricePrecision(150),

  // Builder fee fields, set by the builder's app UI
  builderIdx: 0,           // index in taker's approvedBuilders list
  builderFeeTenthBps: 50,  // fee for this order: 5 bps (50 * 0.1 bps)
});
```
  

The same two fields go on a Swift signed order message, set on the message alongside `signedMsgOrderParams`. See [Sign the order message](/developers/velocity-sdk/swift.md#step-2-sign-the-order-message).

> **Warning:**
>
> `builderFeeTenthBps` must be within **both** caps: the user's `maxFeeTenthBps` for that builder, and the protocol-wide `MAX_BUILDER_FEE_TENTH_BPS` of `1000` (1% of notional), an onchain Rust constant in the velocity program rather than an SDK export. Exceeding either fails the placement.

### Changing a Builder-Coded Order

`modifyOrder` and `modifyOrderByUserOrderId` reject any order that carries a builder code, with `CannotModifyBuilderOrder` (error `6366` / `0x18de`, "Cannot modify a builder-coded order; cancel and re-place instead"). The fee attribution lives in a `RevenueShareEscrow` row keyed to the order's `orderId`, and a modify re-places under a new id, which would strip the attribution.

Cancel and re-place instead. `cancelAndPlaceOrders` does both in one transaction, so the old order is not fillable in the gap:

  
```js
import { MarketType, OrderType, PositionDirection } from "@velocity-exchange/sdk";

// Replace a builder-coded order: cancel, then place with the builder params set again
await userClient.cancelAndPlaceOrders(
  { marketType: MarketType.PERP, marketIndex: 0, direction: PositionDirection.LONG },
  [
    {
      orderType: OrderType.LIMIT,
      marketType: MarketType.PERP,
      marketIndex: 0,
      direction: PositionDirection.LONG,
      baseAssetAmount: userClient.convertToPerpPrecision(1),
      price: userClient.convertToPricePrecision(151),
      builderIdx: 0,
      builderFeeTenthBps: 50,
    },
  ]
);
```
  

A client that reuses one modify path for all order updates should branch on the builder fields before calling it.

## Fill-time enforcement

Perp fills fail onchain with `UnableToLoadRevenueShareAccount` (error `6324` / `0x18b4`) unless the **taker's** `RevenueShareEscrow` is passed as a remaining account when either:

- the taker's order carries a builder code (`hasBuilderParams(orderParams)` is true), or
- the taker's `UserStats.referrerStatus` has the `BuilderReferral` bit set and their escrow has a referrer (`escrowHasReferrer(escrow)`).

Liquidation fills are exempt, as is any fill made while the `BuilderCodes` bit of `FeatureBitFlags` is clear on `State.featureBitFlags`; read the state account for that bit's setting. Fillers and keepers must attach the escrow for any taker that matches one of the above, and the SDK's fill and place-and-make builders accept the taker's decoded escrow directly:

  
```js
import { hasBuilderParams, isBuilderReferral } from "@velocity-exchange/sdk";

// Resolve whether this taker's escrow must be attached
const takerNeedsEscrow =
  hasBuilderParams(takerOrder) || isBuilderReferral(takerUserStats);

const takerEscrow = takerNeedsEscrow
  ? await revenueShareEscrowMap.mustGet(takerAuthority.toString())
  : undefined;

await fillerClient.fillPerpOrder(
  takerUserAccountPublicKey,
  takerUserAccount,
  order,
  makerInfo,
  txParams,
  fillerSubAccountId,
  fillerAuthority,
  hasBuilderFee,
  takerEscrow // attached only when required; validated against taker's authority
);
```
  

`fillPerpOrder` and `getFillPerpOrderIx`, `placeAndTakePerpOrder` and `getPlaceAndTakePerpOrderIx`, `placeAndMakePerpOrder` and `getPlaceAndMakePerpOrderIx`, and `getPlaceAndMakeSignedMsgPerpOrderIxs` all accept this optional trailing `takerEscrow`. The builders validate `takerEscrow.authority` against the taker's authority and throw if they don't match.

For keepers filling many takers, `RevenueShareEscrowMap` caches escrow accounts by authority, which avoids a re-fetch per fill:

  
```js
import { RevenueShareEscrowMap } from "@velocity-exchange/sdk";

const revenueShareEscrowMap = new RevenueShareEscrowMap(fillerClient);
await revenueShareEscrowMap.subscribe();

const escrow = revenueShareEscrowMap.get(takerAuthority.toString()); // undefined if not (yet) cached
```
  

> **Info:**
>
> Referral rewards also only accrue (and referral slots are only created) for escrows that have a referrer: an escrow with no referrer set is exempt from the enforcement above even if the taker's `referrerStatus` bit happens to be set.

Market makers filling as `makerInfo` are unaffected by any of this: the enforcement gates the **taker's** escrow only. A maker never passes its own `RevenueShareEscrow` to fill an order, builder-coded or not.

## Collecting accrued fees

An accrued row leaves escrow through one of three instructions, all paying out of the perp market's PnL pool:

| Method | Caller | Behavior |
| --- | --- | --- |
| `settleRevenueShare` | Anyone | Pays the accrued builder and referrer rows of one escrow on one perp market, without needing the owner. Rejects a delisted market. |
| `settlePNL` | Escrow owner | Runs the same sweep as a side effect, but only when it moves PnL for that market. |
| `forfeitRevenueShareOrder` | Anyone | Writes off one row the program cannot pay. Moves no tokens. |

Do not rely on `settlePNL`. Once the escrow owner closes their position and stops trading, no PnL settle can pay the row, and the market's `pendingRevenueShare` holds PnL-pool value against the claim indefinitely. `settleRevenueShare` exists so a builder or keeper can collect on its own schedule:

  
```js
const escrow = await builderClient.fetchRevenueShareEscrowAccount(takerAuthority);

// Pays this escrow's builder and referrer rows for perp market 0
await builderClient.settleRevenueShare(takerAuthority, escrow, 0);
```
  

The SDK builds `remainingAccounts`: the oracle and market accounts, then the owner's onchain sub-accounts read-only (the program needs them to mark rows `Completed` before paying), then the beneficiaries' `User` and `RevenueShare` accounts writable.

To find the work list, subscribe a `RevenueShareEscrowMap` and ask which escrows still owe on a market. Call `syncAll()` first, because a partial cache under-reports:

  
```js
import { PublicKey } from "@solana/web3.js";

await revenueShareEscrowMap.syncAll();

const owing = revenueShareEscrowMap.getEscrowsOwingRevenueShare(0);
for (const [authority, escrow] of owing) {
  await builderClient.settleRevenueShare(new PublicKey(authority), escrow, 0);
}
```
  

The sweep pays a row only when its `feesAccrued` fits in the payable part of the PnL pool. Check that before sending, to avoid paying for a transaction that settles nothing:

  
```js
import { calculateRevenueShareSweepAvailable } from "@velocity-exchange/sdk";

const perpMarket = builderClient.getPerpMarketAccountOrThrow(0);
const spotMarket = builderClient.getSpotMarketAccountOrThrow(perpMarket.quoteSpotMarketIndex);
const oraclePriceData = builderClient.getOracleDataForPerpMarket(0);

// QUOTE_PRECISION (1e6): pnlPoolTokens - max(netUserPnl, 0) - bankruptcy IF tranche, floored at 0
const available = calculateRevenueShareSweepAvailable(perpMarket, spotMarket, oraclePriceData);
```
  

### Writing off an unpayable row

`forfeitRevenueShareOrder` clears a row so `pendingRevenueShare` can reach zero and the delist is not blocked. The market must be in settlement or delisted, and the program requires proof it cannot pay: the beneficiary has no payout `User` account, the closed pool is smaller than the row, or the row names no beneficiary the program can reach. A still-payable row is rejected with `RevenueShareOrderNotForfeitable` (error `6373` / `0x18e5`).

  
```js
// orderIndex is the row's index within escrow.orders
await builderClient.forfeitRevenueShareOrder(takerAuthority, escrow, 0, orderIndex);
```
  

The SDK resolves the row's beneficiary the same way the program does (the referrer for a referral row, otherwise `approvedBuilders[builderIdx]`) and passes that authority's sub-account 0. The program re-derives the address and rejects any other one.
