> 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/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. The builder must 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 builder fee to charge in tenths of a basis point: a value of `10` means **1 basis point of the order notional** is charged to the user and sent to the builder.

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](/documentation/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 requires that builder account to exist in both engines and to meet the 100 vUSDC requirement in each. These checks also apply when clearing approval with `maxFeeRate: 0`. Submit `maxFeeRate: 0` to free 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 specifies its rate in `builder.fee`. That rate must stay within the user's approved maximum **and** the engine cap in the table above. A Perps order cannot charge more than `200` even when the approval is `2000`.

## Integration flow

1. Resolve the builder account ID as above, and confirm 100 vUSDC in each engine.
2. Query [subaccounts](/documentation/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](/documentation/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 order carrying an unapproved builder is rejected. 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](/documentation/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](/documentation/for-developers/developers/trading/authentication-and-signing.md#how-to-compute-payloadhash) and the [order walkthrough](/documentation/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. Omit the builder from subsequent orders. Manage existing orders separately through the order-cancellation endpoints.

## 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`](/documentation/for-developers/api-reference/trading-api/rest-v1/sodex-rest-spot-api.md#query-balances)   |
| Perps  | [`GET .../perps/accounts/{userAddress}/balances`](/documentation/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](/documentation/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](/documentation/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.

```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);
ws.addEventListener("open", () => ws.send(JSON.stringify({
  op: "subscribe", id: 1,
  params: { channel: "accountUpdate", user: BUILDER_USER_ADDRESS, accountID },
})));
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", () => {
  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 income view. The REST trade field `builderFee` and compact WebSocket field `bf` report the fee on a fill; use them instead of recomputing fees from the approval ceiling.
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](/documentation/for-developers/api-reference/trading-api/rest-v1/sodex-rest-spot-api.md) or [Perps](/documentation/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`  | Builder's Spot balance against the 100 vUSDC eligibility requirement.                                                                                            |
| `perps builder USDC balance is insufficient` | Builder's Perps balance against the 100 vUSDC eligibility requirement.                                                                                           |
| Builder account not found                    | Builder ID is the 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-enabled order rejected               | Approval exists for the relevant engine, requested fee is within the approval and the engine cap (Spot 2%, Perps 0.2%), and builder was included before signing. |

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