Skip to Content

Builder Codes

Builder Codes let integrators earn fees by routing order flow through their application. A builder fee is an extra fee (on top of the taker’s normal tiered fee) attached to an order via builderIdx/builderFeeTenthBps, and settled to the builder’s revenue-share account when the order fills.

Builder codes work on any perp order — regular on-chain order placement (placePerpOrder, placeAndTakePerpOrder, placeAndMakePerpOrder, fillPerpOrder) as well as Swift 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:

import { VelocityClient } from "@velocity-exchange/sdk"; import { Connection, Keypair } from "@solana/web3.js"; // Initialize connection const connection = new Connection("https://api.mainnet-beta.solana.com"); // Builder wallet (revenue share provider) const builderWallet = Keypair.fromSecretKey(/* builder secret key */); const builderAuthority = builderWallet.publicKey; // User wallet (end user) const userWallet = Keypair.fromSecretKey(/* user secret key */); const takerAuthority = userWallet.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();
Example Builder codes setupReference ↗
TypeScript docs unavailable for Builder codes setup.

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:

await builderClient.initializeRevenueShare(builderAuthority);
Method VelocityClient.initializeRevenueShareReference ↗
ParameterTypeRequired
authority
PublicKey
Authority the account is created for.
Yes
txParams
TxParams
Optional compute-unit/priority-fee overrides for the transaction.
No
Returns
Promise<string>

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 you expect across all of this user’s subaccounts:

// numOrders should cover concurrent open orders across all of the user's subaccounts await userClient.initializeRevenueShareEscrow(takerAuthority, 16);
Method VelocityClient.initializeRevenueShareEscrowReference ↗
ParameterTypeRequired
authority
PublicKey
Authority the escrow is created for.
Yes
numOrders
number
Number of pending-order slots to allocate; determines account rent/size. Can be grown later with `resizeRevenueShareEscrowOrders` (never shrunk).
Yes
txParams
TxParams
Optional compute-unit/priority-fee overrides for the transaction.
No
Returns
Promise<string>

On creation, the escrow’s referrer field is copied from the user’s existing UserStats.referrer, if any (see 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:

// maxFeeTenthBps is in tenths of a basis point (100 = 1 bp = 0.01%) await userClient.changeApprovedBuilder(builderAuthority, 200, true);
Method VelocityClient.changeApprovedBuilderReference ↗
ParameterTypeRequired
builder
PublicKey
The public key of the builder to add or update. Must differ from the escrow's own authority.
Yes
maxFeeTenthBps
number
The maximum fee, in tenths of a basis point, the builder may charge.
Yes
add
boolean
Whether to add or update the builder. If the builder already exists, `add = true` will update the `maxFeeTenthBps`, otherwise it will add the builder. If `add = false`, the builder's `maxFeeTenthBps` will be set to 0.
Yes
txParams
TxParams
The transaction parameters to use for the transaction.
No
Returns
Promise<string>

Call with add = false to revoke a builder — this fails with CannotRevokeBuilderWithOpenOrders if that builder has open (unsettled) orders outstanding, which must be cancelled or settled first.

Order Placement (Builder Fee on a Regular Order)

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

import { OrderType, MarketType, 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) });
Example Order with a builder feeReference ↗
TypeScript docs unavailable for Order with a builder fee.

The same two fields are set on a Swift signed order message the same way — see Order Placement (Builder adds fee to Swift order) in the Swift guide.

Fill-time enforcement

Perp fills fail on-chain 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 and the feature-flag-off state are exempt. Fillers/keepers must attach the escrow for any taker that matches one of the above — the SDK’s fill and place-and-make builders accept the taker’s decoded escrow directly:

import { isBuilderReferral, escrowHasReferrer, hasBuilderParams } 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 );
Example takerEscrow on fillsReference ↗
TypeScript docs unavailable for takerEscrow on fills.

fillPerpOrder/getFillPerpOrderIx, placeAndTakePerpOrder/getPlaceAndTakePerpOrderIx, placeAndMakePerpOrder/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 so you don’t re-fetch per fill:

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
Class RevenueShareEscrowMapReference ↗

In-memory cache mapping each authority to their `RevenueShareEscrow` account (builder/referral fee accrual escrow).

PropertyTypeRequired
authorityEscrowMap
any
map from authority pubkey to RevenueShareEscrow account data.
Yes
velocityClient
any
Yes
parallelSync
any
Yes
fetchPromise
any
No
fetchPromiseResolver
any
Yes
subscribe
() => Promise<void>
Populates the map via a one-time `sync()` (no-op if already populated). There is no live/push subscription here — call `sync()`/`slowSync()` again later to pick up new or updated escrow accounts.
Yes
has
(authorityPublicKey: string) => boolean
Returns true if `authorityPublicKey` has a `RevenueShareEscrow` account cached in the map.
Yes
get
(authorityPublicKey: string) => RevenueShareEscrowAccount | undefined
Returns the cached `RevenueShareEscrowAccount` for `authorityPublicKey`, or `undefined` if not (yet) in the map.
Yes
mustGet
(authorityPublicKey: string) => Promise<RevenueShareEscrowAccount | undefined>
Enforce that a RevenueShareEscrow will exist for the given authorityPublicKey, reading one from the blockchain if necessary.
Yes
addRevenueShareEscrow
(authority: string) => Promise<void>
Fetches and decodes `authority`'s `RevenueShareEscrow` account directly via RPC and caches it. If the account does not exist (a normal condition — not every authority has an escrow), logs a debug message and leaves the map entry absent rather than throwing.
Yes
size
() => number
Number of `RevenueShareEscrow` accounts currently cached in the map.
Yes
sync
() => Promise<void>
Fully (re)populates the map via `syncAll` (a `getProgramAccounts` scan). Concurrent calls share the same in-flight promise.
Yes
slowSync
() => Promise<void>
A slow, bankrun test friendly version of sync(), uses getAccountInfo on every cached account to refresh data
Yes
syncAll
() => Promise<void>
Fetches and decodes every `RevenueShareEscrow` program account (via `getRevenueShareEscrowFilter`), in batches of 100 with a 10ms delay between batches to avoid overwhelming the RPC, and caches them keyed by `escrow.authority`. Batch decoding runs in parallel unless constructed with `parallelSync: false`. A decode failure for one account is logged and skipped rather than aborting the whole sync.
Yes
getAll
() => Map<string, RevenueShareEscrowAccount>
Returns a shallow copy of the full authority-to-escrow map (mutating the returned map does not affect the cache).
Yes
getAuthorities
() => string[]
Returns the base58 authority pubkeys of every `RevenueShareEscrow` account currently cached.
Yes
getEscrowsWithApprovedReferrers
() => Map<string, RevenueShareEscrowAccount>
Get `RevenueShareEscrow` accounts that have at least one entry in `approvedBuilders` — builders this user has approved to charge an order fee (not "referrers": `referrer` is a separate field on the account).
Yes
getEscrowsWithOrders
() => Map<string, RevenueShareEscrowAccount>
Get `RevenueShareEscrow` accounts with at least one entry in `orders` — the ring buffer of in-flight builder/referral fee accruals not yet settled via settle-PnL.
Yes
getByReferrer
(referrerPublicKey: string) => RevenueShareEscrowAccount | undefined
Returns the first cached `RevenueShareEscrow` account whose `referrer` field equals `referrerPublicKey`. There is no reverse index, so this is an O(n) scan over every cached escrow; prefer `getAllByReferrer` if more than one escrow may share the same referrer.
Yes
getAllByReferrer
(referrerPublicKey: string) => RevenueShareEscrowAccount[]
Returns every cached `RevenueShareEscrow` account whose `referrer` field equals `referrerPublicKey`. O(n) scan over every cached escrow (no reverse index).
Yes
unsubscribe
() => Promise<void>
Clears the in-memory map. Does not tear down any RPC subscriptions (this class has none — `subscribe` only triggers a one-time sync).
Yes

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.

Notes on builder codes for MMs

Market makers filling as makerInfo are unaffected by this enforcement — it only gates the taker’s escrow. Makers do not need to pass their own RevenueShareEscrow to fill orders, builder-coded or not.

Last updated on