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

# Builder Codes in Trading

The term "builder" here does not refer to ValueChain block builders. Builder codes let application builders receive a fee on fills they send on behalf of a user. They are set per-order. The user must approve a maximum builder fee for each builder, and can revoke that approval at any time.

To use builder codes, the user first approves a max fee for the builder account ID. Sign that action with the user's **master wallet**, not an API key. Granting or updating an approval with a non-zero `maxFeeRate` requires the builder to hold at least **100 vUSDC in Spot** and **100 vUSDC in Perps**.

Builder codes currently only apply to fees collected in the **quote or collateral asset**. They do not apply to the buying side of spot trades; they apply to both sides of perp trades. Builder fees charged can be at most **0.2% on perps** and **2% on spot**.

After approval, orders may include an optional builder parameter: `{"id": number, "fee": number}`. `id` is the builder account ID. `fee` is the requested builder fee rate, expressed in tenths of a basis point. A value of `10` requests **1 basis point of the fill notional**. The actual rate is subject to the user's approval and the engine cap.

Each user can have a maximum of **10** active builder approvals at a time.

## Become a builder

Any **activated** trading account can act as a builder once it holds at least 100 vUSDC in each engine. There is no separate builder registration. The builder ID is that account's numeric account ID.

Read it from [Query Subaccounts](/for-developers/api-reference/trading-api/rest-v1/sodex-rest-public-api.md#query-subaccounts) (`data.primaryAccountID`) or from Spot or Perps account state (`data.aid`).

The standalone approval endpoint checks that the builder account exists in both engines and meets the 100 vUSDC requirement in each when granting or updating an approval with a non-zero `maxFeeRate`. Falling below this balance after approval does not stop builder fee credits. Clearing approval with `maxFeeRate: 0` skips these builder eligibility checks and frees an approval slot.

## Fee units and limits

Both `fee` and `maxFeeRate` are integers measured in **tenths of a basis point**:

| API value | Basis points | Percentage of order notional |
| --------- | ------------ | ---------------------------- |
| `10`      | 1 bp         | 0.01%                        |
| `50`      | 5 bp         | 0.05%                        |
| `200`     | 20 bp        | 0.2%                         |
| `2000`    | 200 bp       | 2%                           |

| Engine | Maximum builder fee | API value |
| ------ | ------------------- | --------- |
| Spot   | 2%                  | `2000`    |
| Perps  | 0.2%                | `200`     |

The approval endpoint accepts `maxFeeRate` from `0` through `2000`, inclusive. Each order requests a rate in `builder.fee`. The actual rate charged is limited to the user's approved maximum **and** the engine cap in the table above. Without approval for that builder, the actual builder fee is zero. The actual builder fee rate for Perps cannot exceed `200` (0.2%), even when `maxFeeRate` is `2000`. Missing approval or a requested rate above either limit does not by itself reject an otherwise valid order; it results in a zero or reduced builder fee.

## Integration flow

1. Resolve the builder account ID as above, and confirm 100 vUSDC in each engine.
2. Query [subaccounts](/for-developers/api-reference/trading-api/rest-v1/sodex-rest-public-api.md#query-subaccounts) and use `data.primaryAccountID` as the approval's `accountID`.
3. Set the builder's numeric account ID and the requested fee rate.
4. [Register a trading API key](/for-developers/developers/trading/api-keys-and-nonces.md#register-a-trading-api-key), then approve the builder with the user's master wallet. Sign these requests separately, or use the combined registration action below.
5. Query `GET /api/v1/user/{userAddress}/builders`. Check `builderID` and `feeRate` in `data.spot` or `data.perps` for each engine the application uses.
6. Include the approved builder in the signed order and track execution.

An otherwise valid order carrying an unapproved builder can still execute, but no builder fee is charged. Check the actual fee on the fill rather than assuming the requested fee was charged. For orders without a builder, omit the `builder` field before signing.

Approval succeeds when both engines accept the request. Queries may lag behind submission. After a timeout, error, or incomplete query result, re-query both engines before retrying with a fresh nonce.

## Approval example

Approve a builder with the user's master wallet. `Client.from_env()` uses Mainnet unless `SODEX_NETWORK=testnet`. Set `SODEX_PRIVATE_KEY` to the master wallet private key, `SODEX_BUILDER_ID` to the builder account ID, and `SODEX_BUILDER_FEE_RATE` to the maximum fee rate.

* Python: [`examples/approve_builder_fee.py`](https://github.com/sodex-tech/sodex-python-sdk-public/blob/main/examples/approve_builder_fee.py)

## EIP-712 signing

Sign account-level actions with the user's master wallet and the following domain:

| Domain field        | Value                                          |
| ------------------- | ---------------------------------------------- |
| `name`              | `universal`                                    |
| `version`           | `1`                                            |
| `chainId`           | The exact `uint64` value sent in `X-API-Chain` |
| `verifyingContract` | `0x0000000000000000000000000000000000000000`   |

For builder actions, the message field `chainID` is different from domain `chainId`: use `286623` for SoDEX Mainnet or `138565` for Testnet, regardless of the wallet's selected network. The SDK example uses the SoDEX network ID for both values. Ordinary `AddAPIKey` registration has no message-level `chainID`; it uses the network ID in the domain.

### Action types

These type strings are case-sensitive, and field order is part of the signature:

```
AddAPIKey(uint64 accountID,string name,uint8 keyType,bytes publicKey,uint64 expiresAt,uint64 nonce)
AddAPIKeyWithBuilder(uint64 chainID,uint64 nonce,uint64 accountID,string name,uint8 keyType,bytes publicKey,uint64 expiresAt,uint64 builderID,uint64 maxFeeRate)
ApproveBuilderFeeAction(uint64 chainID,uint64 nonce,uint64 accountID,uint64 builderID,uint64 maxFeeRate)
```

### Combined API key registration and approval

To register a key and approve a builder in one action, include `builder` in `POST /api/v1/user/{userAddress}/api-keys` and sign the `AddAPIKeyWithBuilder` structure.

| HTTP request field | EIP-712 message field   |
| ------------------ | ----------------------- |
| `type`             | `keyType` (`1` for EVM) |
| `builder.id`       | `builderID`             |
| `builder.fee`      | `maxFeeRate`            |

The remaining message fields follow the type string above. `builder` and `permissions` are mutually exclusive within a registration request.

See [Authentication & Signing](/for-developers/developers/trading/authentication-and-signing.md#add-api-key) for the complete typed-data objects, including the permissioned-key variant.

### Signature bytes and headers

For the account-level actions above, `X-API-Sign` is the hex encoding of `0x02 || r[32] || s[32] || v[1]`, where `v` is `0` or `1`. Wallet libraries that return `27` or `28` must normalize the recovery byte before adding the prefix. Send the same nonce in the message and `X-API-Nonce`, and the domain chain ID in `X-API-Chain`.

Trading orders use the `spot` or `futures` domain and signature prefix `0x01`.

## Order payload and execution

After approval, include this fragment in the order parameters **before computing the payload hash**:

```json
{
  "builder": {
    "id": 456,
    "fee": 50
  }
}
```

Replace `456` with your builder account ID. Spot batches specify `builder` after `orders` at the batch level. Perps supports a batch default and per-order overrides. Use a rate within both the approval and the engine cap.

SDK examples: [Python](https://github.com/sodex-tech/sodex-python-sdk-public/tree/main/examples)

### Canonical JSON

1. Preserve the documented field order in `{type, params}`. `builder` is the last field in its containing order or batch structure, with `id` before `fee`.
2. Omit absent optional fields marked `omitempty`, and retain required zero or false values.
3. Encode `uint64` values as unquoted JSON integers with full precision. Use the SDK's canonical serializer for signed orders.

The signature hashes the `{type, params}` wrapper; the HTTP body contains only `params`. See [payload hashing](/for-developers/developers/trading/authentication-and-signing.md#how-to-compute-payloadhash) and the [order walkthrough](/for-developers/developers/trading/quickstart.md) for signing, submission, and order-status tracking.

Check the top-level response `code` and individual order results. Retain `clOrdID` and the returned order ID for status queries and reconciliation.

### Clear approval

Submit `maxFeeRate: 0` and verify removal in both approval arrays. Clearing approval skips the builder account eligibility and minimum balance checks. The user's account and signature checks still apply. Omit the builder from subsequent orders. Manage existing orders separately through the order-cancellation endpoints.

## Builder statistics

Use the builder account owner's address to query combined Spot and Perps statistics. The optional `accountID` selects a builder account belonging to that owner; omitting it uses the primary account.

| Query                                                                                                                   | Result                                                                                           |
| ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| [Fee summary](/for-developers/api-reference/trading-api/rest-v1/sodex-rest-public-api.md#query-builder-fee-summary)     | Cumulative builder fees, fill count and traded notional, plus the builder user count.            |
| [Daily chart](/for-developers/api-reference/trading-api/rest-v1/sodex-rest-public-api.md#query-builder-fee-daily-chart) | Latest daily records in ascending UTC date order. Defaults to 90 records, with a maximum of 300. |

See [BuilderFeeStats](/for-developers/api-reference/trading-api/rest-v1/schema.md#builderfeestats) and [BuilderFeeDailyStats](/for-developers/api-reference/trading-api/rest-v1/schema.md#builderfeedailystats) for the response fields. These statistics summarize activity; [account balances](#fee-credits-and-withdrawals) show the funds available in each engine.

## Fee credits and withdrawals

Builder fees credit **in real time** to the builder account on the engine that produced the fill. There is no separate claim step. Query that account's balances to see income.

| Engine | REST                                                                                                                                        |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Spot   | [`GET .../spot/accounts/{userAddress}/balances`](/for-developers/api-reference/trading-api/rest-v1/sodex-rest-spot-api.md#query-balances)   |
| Perps  | [`GET .../perps/accounts/{userAddress}/balances`](/for-developers/api-reference/trading-api/rest-v1/sodex-rest-perps-api.md#query-balances) |

Pass the builder account ID as `accountID`. Spot `total` includes locked funds; Perps `total` is wallet balance.

To send credited funds off-chain, use ordinary [Withdrawals](/for-developers/developers/mirror-protocol/lifecycle/withdrawals.md): Perps → Spot → ValueChain EVM → external. Trading `transferAsset` only reaches EVM.

```bash
export BASE_URL="https://mainnet-gw.sodex.dev"
export BUILDER_USER_ADDRESS="<builder-wallet-address>"
export BUILDER_ACCOUNT_ID="<builder-account-id>"

curl -sS "$BASE_URL/api/v1/spot/accounts/$BUILDER_USER_ADDRESS/balances?accountID=$BUILDER_ACCOUNT_ID"
curl -sS "$BASE_URL/api/v1/perps/accounts/$BUILDER_USER_ADDRESS/balances?accountID=$BUILDER_ACCOUNT_ID"
```

An approval sets a fee ceiling; it is not a payment record. Subscribe to the receiving builder account's [Account Updates Stream](/for-developers/api-reference/trading-api/websocket-v1/account-updates.md) for live balance changes, not the user's builder-approval list.

| Engine | Stream                                | Updated balance                                            |
| ------ | ------------------------------------- | ---------------------------------------------------------- |
| Spot   | `wss://mainnet-gw.sodex.dev/ws/spot`  | `data.B[].t` (includes locked funds); `l` is locked funds. |
| Perps  | `wss://mainnet-gw.sodex.dev/ws/perps` | `data.B[].wb` (wallet balance).                            |

Each balance includes coin ID `i` and name `a`. Values are decimal strings representing the updated balance, not an amount to add. Only updated assets appear in `B`; an omitted asset has not necessarily become zero.

### Monitor builder balances

Use Node.js 22+, set `BUILDER_USER_ADDRESS`, `BUILDER_ACCOUNT_ID`, and `ENGINE` (`spot` or `perps`), save as `builder-balances.mjs`, and run `node builder-balances.mjs`. This read-only example prints balance updates without submitting trades and sends a ping every 40 seconds to keep idle connections alive. See the [connection guide](/for-developers/api-reference/trading-api/websocket-v1.md#connection) for heartbeat and reconnect requirements.

```javascript
const { BUILDER_USER_ADDRESS, BUILDER_ACCOUNT_ID, ENGINE } = process.env;
const accountID = Number(BUILDER_ACCOUNT_ID);
if (!BUILDER_USER_ADDRESS || !BUILDER_ACCOUNT_ID ||
    !Number.isSafeInteger(accountID) || accountID < 0 ||
    !["spot", "perps"].includes(ENGINE)) {
  throw new Error("Set builder address, numeric account ID, and ENGINE");
}
const ws = new WebSocket("wss://mainnet-gw.sodex.dev/ws/" + ENGINE);
let heartbeat;
ws.addEventListener("open", () => {
  ws.send(JSON.stringify({
    op: "subscribe", id: 1,
    params: { channel: "accountUpdate", user: BUILDER_USER_ADDRESS, accountID },
  }));
  heartbeat = setInterval(() => ws.send(JSON.stringify({ op: "ping" })), 40_000);
});
ws.addEventListener("message", event => {
  const message = JSON.parse(event.data);
  if (message.op === "subscribe") {
    if (!message.success) {
      console.error("Subscription failed:", message.error);
      ws.close();
    }
    return;
  }
  if (message.channel !== "accountUpdate" || message.type !== "update") return;
  for (const balance of message.data.B ?? []) {
    console.log({
      engine: ENGINE, accountID, block: message.data.h,
      coinID: balance.i, asset: balance.a,
      balance: ENGINE === "spot" ? balance.t : balance.wb,
    });
  }
});
ws.addEventListener("close", () => {
  clearInterval(heartbeat);
  console.error("Disconnected: refresh account balances and resubscribe before continuing");
});
ws.addEventListener("error", () => console.error("WebSocket connection error"));
```

### Reconciliation

1. Query the builder account balances for the current balance view. The REST trade field `builderFee` and compact WebSocket field `bf` report the actual builder fee on a fill; use them instead of recomputing fees from the approval ceiling or requested rate. This fee is already included in REST `fee` and WebSocket `f`; do not add it again to calculate the total fee. An accepted order is not a fill, so use trade records to determine the final fee charged.
2. Compare balance changes over the same interval with expected fee receipts, accounting separately for transfers, trades, and other balance changes.
3. On disconnection, refresh [Spot](/for-developers/api-reference/trading-api/rest-v1/sodex-rest-spot-api.md) or [Perps](/for-developers/api-reference/trading-api/rest-v1/sodex-rest-perps-api.md) account balances and resubscribe. A new snapshot restores the current balance, not the missed transaction history.

`accountUpdate` does not contain a builder-fee reason, per-credit identifier, or trade ID. It shows the balance effect of credits but cannot by itself prove a one-to-one match between a trade fee and a credit. Keep unexplained differences pending; do not classify every positive balance change as builder income.

## Troubleshooting

| Symptom                                      | Check                                                                                                                                                                                                                                                                                     |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `spot builder USDC balance is insufficient`  | When granting or updating an approval with a non-zero `maxFeeRate`, check the builder's Spot balance against the 100 vUSDC eligibility requirement.                                                                                                                                       |
| `perps builder USDC balance is insufficient` | When granting or updating an approval with a non-zero `maxFeeRate`, check the builder's Perps balance against the 100 vUSDC eligibility requirement.                                                                                                                                      |
| Builder account not found                    | For an approval with a non-zero `maxFeeRate`, confirm the builder ID is an existing trading account ID on this network.                                                                                                                                                                   |
| Approval visible in only one engine          | Re-query both approval arrays before retrying.                                                                                                                                                                                                                                            |
| More than 10 builders                        | A user may hold at most 10 approvals; clear one with `maxFeeRate: 0`.                                                                                                                                                                                                                     |
| Signature recovery or API key lookup error   | Signing wallet, primary type, field order, chain IDs, signature encoding, and registered API key name.                                                                                                                                                                                    |
| Builder fee is zero or lower than requested  | Check approval for the relevant engine, the approved maximum, and the engine cap (Spot 2%, Perps 0.2%). Missing approval produces no builder fee; rates above either limit are reduced. These conditions do not by themselves reject the order. Inspect `builderFee` or `bf` on the fill. |

{% content-ref url="/pages/L8fKb3XwZYhG9b9PksBM#approve-builder-fee" %}
[Public](/for-developers/api-reference/trading-api/rest-v1/sodex-rest-public-api.md#approve-builder-fee)
{% endcontent-ref %}
