> For the complete documentation index, see [llms.txt](https://sodex.com/documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sodex.com/documentation/for-developers/developers/wealth/builder.md).

# Builder Integration

A Wealth builder is an address that receives a fee from a user's vault redemption or RFQ sell. Any address can be a builder. There is no registration and no minimum balance. This page is not about ValueChain block production.

Branch on `fundType` before attaching a builder fee.

| `fundType`     | Where it trades                             | Builder fee                                                                                                                          |
| -------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `escrow_vault` | Vault contracts and TigrisRFQ on ValueChain | This page. The fee is one packed `bytes32` on the ValueChain call.                                                                   |
| `clob`         | The orderbook                               | [Builder Codes in Trading](/documentation/for-developers/developers/trading/builder-codes-in-trading.md). The fee is an order field. |

A `clob` fund is a SoDEX RWA token, such as `vQQQ` or `vVOO`. Its buys and sells are orderbook trades. Use the trading builder flow for those fills, and do not pack `builderFeeData` onto a CLOB order. The rest of this page is the `escrow_vault` path.

The user sends the transaction from their own wallet and pays ValueChain gas in SOSO. `approveBuilder` takes no signature. `requestDepositOnBehalf`, `requestRedeemOnBehalf`, `buyOnBehalf`, and `sellOnBehalf` have no `builderFeeData` argument, so the [Transaction Relayer](/documentation/for-developers/developers/valuechain-evm/relayer.md) cannot attach a fee.

Deposits and RFQ buys can update the user's cap for that builder. They do not transfer a token. The fee is taken from the user's proceeds when a redeem or sell claim succeeds, in `vault.getAsset()`, and sent to the builder. The builder does not submit a second claim.

## Fee units and limits

`feeRate` and `maxFeeRate` are integers. Precision is `100_000`. The protocol maximum is `2000`.

```
fee = amount * rate / 100_000
```

| rate   | Basis points | Percent of proceeds |
| ------ | ------------ | ------------------- |
| `10`   | 1 bp         | 0.01%               |
| `100`  | 10 bp        | 0.1%                |
| `500`  | 50 bp        | 0.5%                |
| `2000` | 200 bp       | 2%                  |

The charged fee is rounded down. The user receives `amount - fee`. A fee that rounds down to `0` stays with the user, and `PostChargeBuilderFee` is not emitted.

`approveBuilder` accepts `maxFeeRate` from `1` to `2000`. The rate stored on a redeem or sell is `min(feeRate, cap)`.

## Integration flow

1. Choose the builder address that receives the fee.
2. Load the vault from [Fund Information](/documentation/for-developers/api-reference/wealth-api/funds.md#get-fund-information) for an `escrow_vault` `fundId`. Per-fund addresses change with each deployment.
3. The user sets a cap with `approveBuilder`, or passes a non-zero `maxFeeRate` in `builderFeeData` on the same transaction.
4. Read `getMaxFeeRate(user, builder)`. A result of `0` means this builder is not approved. An unapproved builder with `maxFeeRate` of `0` still lets the redeem or sell succeed, stores nothing, and charges nothing.
5. Encode `builderFeeData`. Encode `referral` separately ([Referrals](/documentation/for-developers/developers/wealth/referrals.md)); use `bytes32(0)` when no code applies. The user submits `requestRedeem` or RFQ `sell`.
6. Index `RedeemBuilderFeeDataLocked` or `SellBuilderFeeDataLocked` on that receipt. The stored rate cannot be read back later. On claim, match `PostChargeBuilderFee` to that request.

## Contracts

ValueChain mainnet, chain id `286623`. RPC `https://mainnet.valuechain.xyz`.

| Contract        | Address                                      |
| --------------- | -------------------------------------------- |
| BuilderRegistry | `0x638c784760FD1eE465F1bDaF721268ACA24D5A22` |
| TigrisRFQ       | `0x4Aa5E39956aCADd68bE0B6720a030Be3C14a6239` |

Load each vault from [Wealth Contracts](/documentation/for-developers/developers/wealth/contracts.md). The vault must be registered with VaultFactory before it can call `approveBuilderFor`. Mainnet TigrisRFQ holds `APPROVE_BUILDER_ROLE`.

## Packing `builderFeeData`

Callers pass one `bytes32`:

| Bits    | Field      | Meaning                                      |
| ------- | ---------- | -------------------------------------------- |
| 0–159   | builder    | Address that receives the fee                |
| 160–175 | feeRate    | Rate requested on this redeem or sell        |
| 176–191 | maxFeeRate | Cap written to BuilderRegistry when non-zero |

`maxFeeRate` of `0` leaves the existing cap unchanged. A non-zero max replaces that user's cap for this builder, including when the new cap is lower. To charge one order without changing the cap, leave max at `0` and call `approveBuilder` first.

```
bytes32(uint256(uint160(builder)) | (feeRate << 160) | (maxFeeRate << 176))
```

```ts
encodeBuilderFee(builder, 500n);            // charge 0.5%, leave the cap unchanged
encodeBuilderFee(builder, 500n, 2_000n);    // set the cap to 2% and charge 0.5%
encodeBuilderFee(builder, 0n, 2_000n);      // deposit: register the cap, charge nothing
```

`feeRate` is ignored on deposit, so a deposit only registers the cap. On redeem or sell the applied rate is `min(feeRate, the cap just read)`. The cap write runs first, so a non-zero `maxFeeRate` in this call is the cap used here. Stored builder fee data keeps the builder and that applied rate, and drops `maxFeeRate`. Decode a stored value with `decodeBuilderFee`: the low 160 bits are the address, and shifting right by 160 then masking with `0xffff` yields the rate.

A zero builder or an applied rate of `0` stores nothing. `RedeemBuilderFeeDataLocked` or `SellBuilderFeeDataLocked` is not emitted.

## Approval

Each user has a cap per builder. `getMaxFeeRate(user, builder) == 0` means the builder is not approved.

```ts
const registryAbi = parseAbi([
  "function approveBuilder(address builder, uint256 maxFeeRate)",
  "function revokeBuilder(address builder)",
  "function getMaxFeeRate(address user, address builder) view returns (uint256)",
]);
```

`approveBuilder` reverts with `ErrInvalidFeeRate` when `maxFeeRate` is outside `1` to `2000`. Later redeems and sells can charge up to this cap without sending a new max.

A separate approval transaction is optional. A non-zero `maxFeeRate` in `builderFeeData` makes the vault or TigrisRFQ call `approveBuilderFor` in the same transaction the user sends. The cap that is checked is the caller's.

`revokeBuilder(builder)` applies to later requests. A redeem or sell that already stored builder fee data still charges that rate.

An unapproved builder with `maxFeeRate` of `0` does not revert. The call succeeds, the stored fee is `bytes32(0)`, no lock event is emitted, and the claim pays the full amount. A zero builder with `maxFeeRate` of `0`, or a `feeRate` of `0`, ends the same way.

`maxFeeRate` above `2000` reverts the whole call with `ErrInvalidFeeRate`, including a deposit. A zero builder with a non-zero `maxFeeRate` reverts with `ErrInvalidAddress`.

## When the fee is charged

| Call                                         | Behavior                                                                        |
| -------------------------------------------- | ------------------------------------------------------------------------------- |
| `requestDeposit`, RFQ `buy`                  | May update the approval. No token is charged and no builder fee data is stored. |
| `requestRedeem`, RFQ `sell`                  | Stores the builder and `min(feeRate, cap)` after this call's cap update.        |
| `claimRedeem`, `claimDelayedRedeem`          | Charges the stored rate on that slice of assets.                                |
| `claimSell`, `claimDelayedSell`              | Charges only the filled underlying proceeds.                                    |
| Unfilled refunds, rejected sells, buy claims | No fee.                                                                         |

The immediate and delayed amounts are each charged at the rate stored for that request. `revokeBuilder` after the request does not change it.

## Redeem, sell, and claim

`requestDeposit` and `requestRedeem` take `builderFeeData` and then `referral`. Pass `bytes32(0)` for either value when it does not apply. Direct calls from the user's wallet are on [Direct Deposits](/documentation/for-developers/developers/wealth/vault-lifecycle/direct-deposits.md) and [Direct Redemptions](/documentation/for-developers/developers/wealth/vault-lifecycle/direct-redemptions.md).

Install `viem` and `tsx`. Set `PRIVATE_KEY`, `VAULT_ADDRESS`, `RECEIVER`, and `RAW_AMOUNT` (share base units). Set `BUILDER` to the fee recipient and `FEE_RATE` to the rate for this redeem. Set `MAX_FEE_RATE` to register a cap in this transaction; omit it to keep the existing cap. Set `REFERRAL` to the `bytes32` from Referrals, or omit it.

Save as `request-redeem-builder.mts` and run `npx tsx request-redeem-builder.mts`.

```typescript
import {
  createPublicClient, createWalletClient, defineChain, http, parseAbi, parseEventLogs, zeroHash,
  type Address, type Hex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";

const MAX_BUILDER_FEE_RATE = 2_000n;
const RATE_MASK = 0xffffn;
const ADDRESS_MASK = (1n << 160n) - 1n;
const BUILDER_REGISTRY = "0x638c784760FD1eE465F1bDaF721268ACA24D5A22" as Address;

function encodeBuilderFee(builder: Address, feeRate: bigint, maxFeeRate = 0n): Hex {
  if (feeRate > RATE_MASK || maxFeeRate > RATE_MASK) {
    throw new Error("fee rate must fit in 16 bits");
  }
  const packed = BigInt(builder) | (feeRate << 160n) | (maxFeeRate << 176n);
  return `0x${packed.toString(16).padStart(64, "0")}`;
}

function decodeBuilderFee(data: Hex) {
  const packed = BigInt(data);
  const builder = `0x${(packed & ADDRESS_MASK).toString(16).padStart(40, "0")}` as Address;
  return {
    builder,
    feeRate: (packed >> 160n) & RATE_MASK,
    maxFeeRate: (packed >> 176n) & RATE_MASK,
  };
}

const valuechain = defineChain({
  id: 286623, name: "ValueChain",
  nativeCurrency: { name: "SOSO", symbol: "SOSO", decimals: 18 },
  rpcUrls: { default: { http: ["https://mainnet.valuechain.xyz"] } },
});
const account = privateKeyToAccount(process.env.PRIVATE_KEY as Hex);
const publicClient = createPublicClient({ chain: valuechain, transport: http() });
const walletClient = createWalletClient({ account, chain: valuechain, transport: http() });
const vault = process.env.VAULT_ADDRESS as Address;
const receiver = process.env.RECEIVER as Address;
const shares = BigInt(process.env.RAW_AMOUNT!);
const referral = (process.env.REFERRAL as Hex | undefined) ?? zeroHash;
const builder = process.env.BUILDER as Address;
const feeRate = BigInt(process.env.FEE_RATE!);
const maxFeeRate = BigInt(process.env.MAX_FEE_RATE ?? "0");
if (!builder || feeRate === 0n) throw new Error("Set BUILDER and a non-zero FEE_RATE");
if (maxFeeRate !== 0n && (maxFeeRate < 1n || maxFeeRate > MAX_BUILDER_FEE_RATE)) {
  throw new Error("MAX_FEE_RATE must be from 1 to 2000");
}

const registryAbi = parseAbi([
  "function getMaxFeeRate(address user, address builder) view returns (uint256)",
  "function approveBuilder(address builder, uint256 maxFeeRate)",
]);
const vaultAbi = parseAbi([
  "function getMinRedeemShares() view returns (uint256)",
  "function balanceOf(address account) view returns (uint256)",
  "function requestRedeem(uint256 shares, address receiver, bytes32 builderFeeData, bytes32 referral)",
  "event RequestedRedeem(address caller, address owner, address receiver, uint256 requestId, uint256 shares, bytes32 referral)",
  "event RedeemBuilderFeeDataLocked(uint256 indexed requestId, bytes32 lockedFeeData)",
]);

if (maxFeeRate === 0n) {
  const approved = await publicClient.readContract({
    address: BUILDER_REGISTRY, abi: registryAbi, functionName: "getMaxFeeRate",
    args: [account.address, builder],
  });
  if (approved === 0n) {
    throw new Error("Builder is not approved. Set MAX_FEE_RATE or call approveBuilder first. An unapproved redeem succeeds and charges nothing.");
  }
}

const [minShares, balance] = await Promise.all([
  publicClient.readContract({ address: vault, abi: vaultAbi, functionName: "getMinRedeemShares" }),
  publicClient.readContract({ address: vault, abi: vaultAbi, functionName: "balanceOf", args: [account.address] }),
]);
if (shares < minShares) throw new Error(`Shares below min redeem: ${minShares}`);
if (balance < shares) throw new Error(`Insufficient vault shares: have ${balance}`);

const builderFeeData = encodeBuilderFee(builder, feeRate, maxFeeRate);
const hash = await walletClient.writeContract({
  address: vault, abi: vaultAbi, functionName: "requestRedeem",
  args: [shares, receiver, builderFeeData, referral],
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error("requestRedeem reverted");

const [requested] = parseEventLogs({ abi: vaultAbi, logs: receipt.logs, eventName: "RequestedRedeem" });
if (!requested) throw new Error("RequestedRedeem was not in the receipt");
const [locked] = parseEventLogs({ abi: vaultAbi, logs: receipt.logs, eventName: "RedeemBuilderFeeDataLocked" });
const lockedFeeData = locked?.args.lockedFeeData;
console.log({
  requestTxHash: hash,
  requestId: requested.args.requestId,
  lockedFeeData: lockedFeeData ?? null,
  charged: lockedFeeData ? decodeBuilderFee(lockedFeeData) : null,
});
```

To set the cap in its own transaction, send `approveBuilder` with the same wallet client before a redeem that leaves `maxFeeRate` at `0`:

```typescript
await walletClient.writeContract({
  address: BUILDER_REGISTRY,
  abi: registryAbi,
  functionName: "approveBuilder",
  args: [builder, 2_000n],
});
```

A deposit uses the same `builderFeeData` position and does not charge a token:

```solidity
function requestDeposit(uint256 assets, address receiver, bytes32 builderFeeData, bytes32 referral) external;
function requestRedeem(uint256 shares, address receiver, bytes32 builderFeeData, bytes32 referral) external;
```

Claim calls do not take `builderFeeData`. The fee was stored on the request. Settlement timing is on [Settlement and Claims](/documentation/for-developers/developers/wealth/vault-lifecycle/settlement-and-claims.md).

```solidity
function claimRedeem(address receiver, uint256 requestId) external;
function claimDelayedRedeem(address receiver, uint256 requestId) external;
function claimSell(address vault, uint256 orderId) external;
function claimDelayedSell(address vault, uint256 orderId) external;
```

### TigrisRFQ

The quote signature covers `payAmount` and `receiveAmount` only. The user attaches `builderFeeData` beside it. `buy` and `sell` return no id. Read `orderId` from `BuySubmitted` or `SellSubmitted` on the receipt. On `SellSubmitted`, `vault`, `owner`, and `orderId` are indexed. `SellBuilderFeeDataLocked` is on the same receipt when the stored fee is non-zero.

```solidity
function buy(address vault, uint256 payAmount, uint256 receiveAmount, uint256 deadline, bytes quoteSignature, bytes32 builderFeeData) external;
function sell(address vault, uint256 payAmount, uint256 receiveAmount, uint256 deadline, bytes quoteSignature, bytes32 builderFeeData) external;

event SellSubmitted(
    address indexed vault,
    address indexed owner,
    uint256 indexed orderId,
    uint256 payAmount,
    uint256 receiveAmount,
    bytes32 orderHash
);
event SellBuilderFeeDataLocked(address indexed vault, uint256 indexed orderId, bytes32 lockedFeeData);
```

A buy can carry `builderFeeData` to update the approval. Claiming a buy does not charge a builder fee. After a sell fills, the user receives the quoted underlying minus the builder fee. A partial fill charges only the filled underlying. A rejected sell charges nothing.

## Accounting

`RedeemBuilderFeeDataLocked` is emitted by the vault proxy. `SellBuilderFeeDataLocked`, and `PostChargeBuilderFee` on a sell claim, are emitted by the TigrisRFQ proxy. `PostChargeBuilderFee` on a vault claim is emitted by the vault proxy.

```solidity
event RedeemBuilderFeeDataLocked(uint256 indexed requestId, bytes32 lockedFeeData);
event SellBuilderFeeDataLocked(address indexed vault, uint256 indexed orderId, bytes32 lockedFeeData);
event PostChargeBuilderFee(address builder, address owner, address token, uint256 amount);
```

`PostChargeBuilderFee.builder`, `owner`, and `token` are not indexed. Filter logs by the proxy address, then match in the client. `token` is the vault underlying, `vault.getAsset()`. On a vault claim, `owner` is the redeem receiver. On a sell claim, `owner` is the order owner. `RequestedRedeem.caller` is the share owner whose cap was checked.

`ClaimedRedeem`, `ClaimedDelayedRedeem`, `Claimed`, and `ClaimedDelayed` report the amount before the fee. The user receives that amount minus `PostChargeBuilderFee.amount`.

1. Read `RedeemBuilderFeeDataLocked` or `SellBuilderFeeDataLocked` from the request receipt. Match it to `RequestedRedeem` or `SellSubmitted` by `requestId` or `orderId`. No lock event means the claim pays the full amount from the claim event.
2. Decode `lockedFeeData`. Estimate `claimableAssets * feeRate / 100_000` for that slice. Use the claimable proceeds, not the approval cap.
3. On the claim receipt, subtract `PostChargeBuilderFee.amount` from the claim-event amount. A fee that rounds down to zero has no `PostChargeBuilderFee`, and the user receives the full claim-event amount.
4. A positive token balance on the builder address still includes transfers other than builder fees. Treat a credit as builder income when a `PostChargeBuilderFee` log names that builder and token.

## Troubleshooting

| Symptom                                            | Check                                                                                                                                                                  |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Call succeeds and the builder receives nothing     | The builder was not approved and `maxFeeRate` was `0`, `feeRate` was `0`, or the fee rounded down to `0`. No lock event means nothing was stored.                      |
| User receives less than the claim event amount     | `ClaimedRedeem`, `ClaimedDelayedRedeem`, `Claimed`, and `ClaimedDelayed` report the pre-fee amount. Subtract `PostChargeBuilderFee.amount`.                            |
| The whole transaction reverts, including a deposit | `maxFeeRate` above `2000` (`ErrInvalidFeeRate`), or a zero builder with a non-zero `maxFeeRate` (`ErrInvalidAddress`). The vault must be registered with VaultFactory. |
| Charged rate is below the rate in `builderFeeData` | The stored rate is `min(feeRate, cap)`. A non-zero `maxFeeRate` in this call replaces the previous cap, including when it is lower.                                    |
| Relayer or `OnBehalf` call cannot attach a fee     | Those methods have no `builderFeeData` argument. `approveBuilder` has no signature; the user sends it.                                                                 |

{% content-ref url="/pages/kJiDvlZsE6kiCK4OskOS" %}
[Builder Codes in Trading](/documentation/for-developers/developers/trading/builder-codes-in-trading.md)
{% endcontent-ref %}

{% content-ref url="/pages/1iM5s4DAQTw3afqqUnG3" %}
[Wealth Contracts](/documentation/for-developers/developers/wealth/contracts.md)
{% endcontent-ref %}

{% content-ref url="/pages/0JPfycqwKsnQ2suYPttG" %}
[Referrals](/documentation/for-developers/developers/wealth/referrals.md)
{% endcontent-ref %}

{% content-ref url="/pages/2cTpaiOb6PElB7vmrlsr" %}
[Direct Redemptions](/documentation/for-developers/developers/wealth/vault-lifecycle/direct-redemptions.md)
{% endcontent-ref %}

{% content-ref url="/pages/Y6UGtOgpGCVoTe1N27nr" %}
[Settlement and Claims](/documentation/for-developers/developers/wealth/vault-lifecycle/settlement-and-claims.md)
{% endcontent-ref %}
