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:
- Builder initializes a
RevenueShareaccount — a one-time, per-authority setup that lets the builder accrue fee-share rewards. - User initializes a
RevenueShareEscrowaccount — tracks the builders this user has approved and any pending (unsettled) builder-fee orders. - 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 ↗
Example Builder codes setupReference ↗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 ↗
Method VelocityClient.initializeRevenueShareReference ↗| Parameter | Type | Required |
|---|---|---|
authority | PublicKeyAuthority the account is created for. | Yes |
txParams | TxParamsOptional 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 ↗
Method VelocityClient.initializeRevenueShareEscrowReference ↗| Parameter | Type | Required |
|---|---|---|
authority | PublicKeyAuthority the escrow is created for. | Yes |
numOrders | numberNumber of pending-order slots to allocate; determines account rent/size. Can
be grown later with `resizeRevenueShareEscrowOrders` (never shrunk). | Yes |
txParams | TxParamsOptional 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 ↗
Method VelocityClient.changeApprovedBuilderReference ↗| Parameter | Type | Required |
|---|---|---|
builder | PublicKeyThe public key of the builder to add or update. Must differ from the escrow's
own authority. | Yes |
maxFeeTenthBps | numberThe maximum fee, in tenths of a basis point, the builder may charge. | Yes |
add | booleanWhether 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 | TxParamsThe 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 ↗
Example Order with a builder feeReference ↗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.referrerStatushas theBuilderReferralbit 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 ↗
Example takerEscrow on fillsReference ↗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) cachedClass RevenueShareEscrowMapReference ↗
Class RevenueShareEscrowMapReference ↗In-memory cache mapping each authority to their `RevenueShareEscrow` account (builder/referral fee accrual escrow).
| Property | Type | Required |
|---|---|---|
authorityEscrowMap | anymap 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) => booleanReturns true if `authorityPublicKey` has a `RevenueShareEscrow` account cached in the map. | Yes |
get | (authorityPublicKey: string) => RevenueShareEscrowAccount | undefinedReturns 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 | () => numberNumber 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 | undefinedReturns 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.