Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Builder Codes

Builder Codes let an application charge its own fee on the orders it routes. The SDK covers the whole lifecycle: the user's approval, the per-order attachment, the approval and reward reads, and the builder's claim.

import { TxFlow } from 'txflow-typescript-sdk'
 
const sdk = new TxFlow('testnet')

Fee units

The same fee appears in three different representations, so convert deliberately:

WhereFieldFormatExample
approveBuilderFeemaxFeeRatePercentage string, greater than 0% and at most 1%'0.02%'
order attachmentbuilder.fInteger in tenths of a basis point, rate = f / 100_00010 is 0.01%
sdk.info.maxBuilderFeereturn valueInteger in tenths of a basis point, 0 when unapproved20 is 0.02%
sdk.info.approvedBuildersmaxFeeRatePercentage string'0.02%'

Approve a builder

approveBuilderFee must be signed by the user's owner wallet. An authorized trading agent cannot approve on the owner's behalf.

sdk.exchange.setWallet(ownerWallet)
 
const result = await sdk.exchange.approveBuilderFee({
  builder: '0x2e4D75df2eE2653EA7a113cC1Fa36D45E923061F',
  maxFeeRate: '0.02%',
})
 
if (result.status !== 'ok') {
  throw new Error(String(result.response))
}

Submitting the action again for the same builder updates the rate instead of consuming another approval slot. Protocol limits — 20 builders per user, and the rejected builder addresses — are listed under approveBuilderFee.

Attach a builder to orders

After the approval is on-chain, order actions may carry one action-level builder object. It is signed together with the orders, so the SDK's normal agent signer applies:

sdk.exchange.setPrivateKey(agentPrivateKey)
 
await sdk.exchange.placeLimitOrder({
  asset: 1,
  side: 'buy',
  px: '60000',
  sz: '0.001',
  tif: 'gtc',
  builder: {
    b: '0x2e4D75df2eE2653EA7a113cC1Fa36D45E923061F',
    f: 10,
  },
})

placeMarketOrder accepts the same optional builder. For batches, placeOrder takes it as a third argument and applies it once to the whole action:

await sdk.exchange.placeOrder(
  [
    { asset: 1, isBuy: true, limitPx: '60000', sz: '0.001', orderType: { limit: { tif: 'gtc' } }, reduceOnly: false },
    { asset: 2, isBuy: false, limitPx: '3000', sz: '0.01', orderType: { limit: { tif: 'gtc' } }, reduceOnly: false },
  ],
  'na',
  { b: '0x2e4D75df2eE2653EA7a113cC1Fa36D45E923061F', f: 10 },
)

Read approvals

const user = '0xOwnerAddress'
const builder = '0x2e4D75df2eE2653EA7a113cC1Fa36D45E923061F'
 
const approvals = await sdk.info.approvedBuilders({ user })
const approvedRate = await sdk.info.maxBuilderFee({ user, builder })

Both methods return either the data or an error object, so narrow the result before using it:

if (!Array.isArray(approvals)) {
  throw new Error(approvals.message)
}
 
for (const approval of approvals) {
  console.log(approval.builder, approval.maxFeeRate)
}
 
if (typeof approvedRate !== 'number') {
  throw new Error(approvedRate.message)
}
 
const canAttach = approvedRate >= 10

approvedBuilders returns a top-level array sorted by builder address. maxBuilderFee returns 0 when the builder has no active approval.

Builder state and rewards

builderState describes the builder's own account, so pass the builder address in user:

const state = await sdk.info.builderState({ user: builderAddress })
 
if ('message' in state) {
  throw new Error(state.message)
}
 
console.log(state.claimableRewards, state.eligible)
FieldMeaning
accruedRewardsTotal fees earned so far
claimableRewardsCurrently unclaimed balance
claimedRewardsAlready claimed total
eligibleWhether the builder currently accrues fees
perpsAccountValueAvailable perp-account balance backing eligibility
isStandardAccountWhether the account type can act as a builder
activeUserCountUsers currently approving the builder

Builder fees are additional to exchange fees and accrue only while the builder holds at least 100 USDC in its perpetual account.

Builder analytics dashboard

Six additional Info reads power a Builder's analytics dashboard. Like builderState, they take the builder wallet address in user, and return aggregate metrics rather than the on-chain account snapshot.

MethodUse
builderMeOverview({ user })Lifetime users, volume, earned fees, claim totals, rank, and weekly stats
builderMeDailySeries({ user, metric?, startDate?, endDate? })Daily fee or user-count series with cumulative totals
builderMeShare({ user, dimension?, window? })Fee share by trading user or instrument
builderMeEarned({ user, startTime?, endTime?, page?, pageSize? })Paginated earnings aggregated by order
builderMeClaimed({ user, startTime?, endTime?, page?, pageSize? })Paginated reward claim history
builderMeClaimable({ user })Current claimable balance and lifetime earned/claimed totals
const builderAddress = '0xBuilderAddress'
 
const [overview, claimable, earned] = await Promise.all([
  sdk.info.builderMeOverview({ user: builderAddress }),
  sdk.info.builderMeClaimable({ user: builderAddress }),
  sdk.info.builderMeEarned({
    user: builderAddress,
    startTime: Date.now() - 7 * 24 * 60 * 60 * 1000,
    page: 0,
    pageSize: 50,
  }),
])

In builderMeEarned, each row's user field is the masked trading-user address, not the builder. Multiple fills for the same order are merged into one row; total counts orders, not fills.

Wire schemas and field descriptions are in the Info API Builder section.

claimBuilderRewards takes no parameters and moves the whole claimable balance. It must be signed by an agent authorized by the builder account, and fails when the claimable balance is zero:

const claim = await sdk.exchange.claimBuilderRewards()
 
if (claim.status !== 'ok') {
  console.error(claim.response)
}

Revoke an approval

sdk.exchange.setWallet(ownerWallet)
 
await sdk.exchange.revokeBuilderFee({
  builder: '0x2e4D75df2eE2653EA7a113cC1Fa36D45E923061F',
})

This is owner-signed like the approval, and fails with Builder not approved when no active approval exists. After propagation the approval's maxFeeRate is 0.

Signer requirements

MethodSigner
sdk.exchange.approveBuilderFeeThe user's owner wallet
sdk.exchange.revokeBuilderFeeThe user's owner wallet
sdk.exchange.placeOrder with builderThe user's trading agent, as for any order
sdk.exchange.claimBuilderRewardsAn agent authorized by the builder account
sdk.info.* builder readsNone

Switch signers with setWallet or setPrivateKey before an owner-signed action, or keep separate clients for the owner wallet, the trading agent, and the builder's agent. See Authentication.

The two approval actions use direct EIP-712 signing, with distinct ApproveBuilderFee and RevokeBuilderFee primary types. Browser wallets sign with their connected chain as domain.chainId, and the SDK forwards that value as the payload-level signatureChainId; the business chainId inside the signed struct remains unchanged. claimBuilderRewards continues to use the tagged Agent flow. Protocol details are under Builder-fee actions.

Standalone functions

Every builder method has a tree-shakable equivalent:

import {
  approveBuilderFee,
  approvedBuilders,
  builderMeClaimable,
  builderMeClaimed,
  builderMeDailySeries,
  builderMeEarned,
  builderMeOverview,
  builderMeShare,
  builderState,
  claimBuilderRewards,
  createExchangeContext,
  createInfoContext,
  maxBuilderFee,
} from 'txflow-typescript-sdk'
 
const info = createInfoContext('testnet')
const state = await builderState(info, { user: builderAddress })
const overview = await builderMeOverview(info, { user: builderAddress })
 
const exchange = createExchangeContext({
  network: 'testnet',
  privateKey: builderAgentPrivateKey,
})
 
await claimBuilderRewards(exchange)

claimBuilderRewards takes only the context; the other functions use fn(context, params).

Protocol reference

Next steps