Velocity ProtocolDevelopers

Velocity Builder Codes

A frontend that routes order flow to Velocity has no way to charge for it. It can build the interface, hold the users, and send the orders, and every basis point of the fee goes to the protocol and its makers. The usual workarounds are worse than the problem: run a separate backend and take custody, or wrap the program and add latency.

Velocity Builder Codes (VBC) close that gap in the program itself. An order carries an identifier for the app that built it and a fee rate the user has approved in advance. The fill charges that fee on top of the taker fee, credits it to the builder, and the builder collects it later. No custody, no wrapper program, no relationship between the builder and the maker who filled the order.

Builder fees are denominated in tenths of a basis point throughout the protocol: 1 unit = 0.001%, so 100 = 10 bps = 0.1%, and 1_000 = 100 bps = 1%. The fields builderFeeTenthBps, feeTenthBps, and maxFeeTenthBps all use this unit.

What the fee is

builder_fee = notional × fee_tenth_bps / 100_000

The builder fee is charged on top of the taker fee. It does not reduce the taker's fee tier or the protocol's cut of the fill, and it accrues to the builder's RevenueShareEscrow rather than to any protocol pool.

How the builder fee sits on top of the taker fee

The normal tiered taker fee, unaffected by the builder feeBuilder fee: notional x fee_tenth_bps / 100,000, capped at 1% of notional, accrues to the taker's RevenueShareEscrow and is swept to the builderWhat the taker paystaker fee + builder feeTaker feesplit per Fee MechanicsBuilder feeto the builder's escrow
Shows the builder fee as a separate charge added on top of the taker fee, not a cut taken from it. Source: the prose on this page. fee_tenth_bps varies per order, so the two legs are drawn with equal widths; the figure shows the two charges, not their sizes. The builder fee is waived on a fill where the taker does not clear initial margin or a required oracle is invalid; the fill itself still happens.

Two ceilings apply, and they are independent. The user sets maxFeeTenthBps per builder when approving one, which the program accepts with no upper bound of its own. On top of that, MAX_BUILDER_FEE_TENTH_BPS caps the fee actually charged at 1000 tenth-bps, 1% of the filled notional. An order carrying more than that is rejected with InvalidBuilderFee (error 6319 / 0x18af); an order above the user's own approved maximum is rejected too. See Trading Fees for the taker fee this sits on top of.

The rail sits behind a feature bit

State.featureBitFlags carries BuilderCodes (0b00000100), and the whole rail is gated on it. Setting the bit takes a deliberate update_feature_bit_flags_builder_codes call with enable: true, which only the cold admin may sign. The same instruction with enable: false clears the bit, and the cold, warm, or hot feature-flag key can sign that. Read the bit off State rather than assuming either state.

While the bit is set, the fill paths load the taker's RevenueShareEscrow and the accrual described on this page runs. While it is clear, no fill path loads that escrow at all: fillPerpOrder, placeAndTakePerpOrder, and the signed-message paths all pass None in its place, so the program treats every order as if it had no builder code. No fee is charged and nothing accrues.

This is not only about builder fees. Referrer rewards and referee discounts ride the same RevenueShareEscrow, so while the bit is clear neither leg of the referral program is computed either: the escrow is never loaded at fill, settlePnl skips the sweep, and settleRevenueShare rejects. An integration that depends on referral or builder rewards should read the bit before building against either leg, because the rail is inert while the bit is clear.

Two of those three are silent. settleRevenueShare is the exception and reverts outright with a DefaultError and the log builder codes feature is disabled.

Setting it up

Builders need a Velocity account and a RevenueShareAccount, created with initializeRevenueShare. That account is where collected fees land.

Users need a RevenueShareEscrow before they can approve anyone, created with initializeRevenueShareEscrow. numOrders sets how many accrual rows the escrow can hold and must be at least 1, because an escrow with no rows silently suppresses every fee, discount, and reward rather than failing. The escrow also requires the authority's first subaccount to already exist. Capacity can be grown later, permissionlessly, with resizeRevenueShareEscrowOrders; it can never be shrunk.

With the escrow in place, the user approves a builder and a maximum fee with changeApprovedBuilder, passing the builder's pubkey and maxFeeTenthBps. The approval is stored inside the user's own RevenueShareEscrow, and a user can approve several builders, each with its own maximum.

The fee's life cycle

Order placement

The builder's app places an order (for example placePerpOrder) with builderIdx and builderFeeTenthBps set in the order params, and includes the RevenueShareEscrow account in the transaction so the program can validate the approval and record the order.

Accrual, per fill

On each fill the fee is credited to the user's RevenueShareEscrow as a RevenueShareOrder row tracking builderIdx, feesAccrued, orderId, feeTenthBps, the market index and type, and a completion flag. Fees stay in escrow until settlement.

Settlement

Accrued rows are swept to the builder's RevenueShareAccount when the user calls settlePnl, or permissionlessly through settleRevenueShare. Rows that can never be paid, for example on a delisted market, are written off with forfeitRevenueShareOrder.

Code for each step is in Builder Codes (SDK).

When the fee is not charged

A builder fee is a transfer out of the taker's account, so the program makes it clear the same gate a withdrawal clears: initial margin, under strict oracle rules. Each price is the more conservative of the live price and the TWAP, invalid deposit oracles contribute no collateral, and every liability oracle must be valid.

If the taker is below initial margin at fill time, or an oracle the gate needs is not trustworthy, the fee is waived. The fill still happens, the taker still closes or opens the position, and the OrderActionRecord still reports the builder, but the builder is paid nothing for that fill. Liquidation fills waive it too.

The margin state is read before the fill, so a position-reducing fill that would restore initial margin still waives the fee for that fill. That is deliberate. Without the gate, a taker below initial margin could reduce a position in slices, route up to 1% of each slice to a builder it approved (possibly itself), and compound the effect as each slice lowered the maintenance requirement, moving value the initial-margin gate is supposed to hold in the account.

A builder-coded order cannot be modified

modifyOrder and modifyOrderByUserOrderId reject any order carrying a builder code, with CannotModifyBuilderOrder (error 6366 / 0x18de, "Cannot modify a builder-coded order; cancel and re-place instead").

The fee attribution for such an order lives in a RevenueShareEscrow row keyed to that order's orderId. A modify cancels and re-places under a new order id, which would leave the row orphaned and silently drop the builder fee. To change a builder-coded order, cancel it and place a new one with the builder params set again. The SDK's cancelAndPlaceOrders does both in one transaction.

Collecting accrued fees

Three instructions move a row out of escrow and into the builder's RevenueShare account. All three pay out of the perp market's P&L pool.

InstructionWho can call itWhat it does
settlePnlThe escrow ownerRuns the sweep after settling the owner's P&L on that market. Only sweeps while the owner still has P&L to settle.
settleRevenueShareAnyonePays the accrued builder and referrer rows in one escrow for one perp market, without the owner settling P&L. Takes marketIndex and the number of owner subaccounts to pass.
forfeitRevenueShareOrderAnyoneWrites off one row the program cannot pay, so PerpMarket.pendingRevenueShare can reach zero and the delist is not blocked. Moves no tokens.

settleRevenueShare exists because settlePnl is not a reliable payout path for a builder. Once the escrow owner closes the position and stops trading, nobody can collect through P&L settlement, and pendingRevenueShare holds P&L-pool value against the claim indefinitely. Builders and keepers should treat settleRevenueShare as their own collection path rather than waiting on the user. It requires the exchange unpaused, settle-P&L unpaused, and the BuilderCodes bit set, and it rejects a delisted market with MarketDelisted, since a delist drains the P&L pool and leaves nothing to pay from.

forfeitRevenueShareOrder needs proof that the row is unpayable, and the market must be in settlement or delisted. One of these must hold: 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 row that can still be paid is rejected with RevenueShareOrderNotForfeitable (error 6373 / 0x18e5).

For makers and fillers

Filling an order that carries a builder code means passing an extra account, and getting it wrong reverts the fill.

Always include the taker's escrow

The RevenueShareEscrow is a PDA derived from the user's pubkey, so finding it costs no RPC call. Omitting it when the order carries a builder code, or when the taker is referred, fails with UnableToLoadRevenueShareAccount (error 6324 / 0x18b4). Because the cost of including it is one account and the cost of omitting it is a reverted transaction, include it on every fill.

The check runs on the taker, and only under two conditions: the BuilderCodes bit must be set on State, and the fill must not be a liquidation. liquidatePerpWithFill never loads an escrow under any flag state, so the liquidatee's forced fill needs no escrow account. "Referred" here means UserStats.referrerStatus carries the BuilderReferral bit (0b00000100), which the program sets when the authority initializes an escrow while it has a referrer. referrerStatus is a bitmask, not a single-valued enum: test the bit rather than comparing the field.

The escrow is an optional trailing remaining account resolved by peeking at its discriminator, so passing an account of the wrong type or size reads as "no escrow passed" and raises the same UnableToLoadRevenueShareAccount. Passing a real escrow belonging to someone else fails earlier and differently, with RevenueShareEscrowAuthorityMismatch.

Expect several builders per user

A user can approve multiple builders, so a maker may see several builder accounts to sweep to during settlePnl. The program does not throw if a particular builder is omitted from the sweep, but every filled row has to be settled eventually, and until it is, its value is held against the market's P&L pool.

See the order matching bot tutorial for the fill call with the escrow and referral arguments wired up.