Builder Codes
A builder code identifies your integration — a trading terminal, bot, or app — to Perpl. It lets you charge your own fee on the orders you route (on top of the protocol fee, attributed to your code and settled to you), and it gives you volume attribution even when you charge nothing.
Builder codes are optional. Everything in Authentication and the WebSocket API works without one — a builder code only adds fee attribution on top.
Three things have to line up:
Perpl registers your builder code — see Registering as a builder.
Each user's API key is enrolled bound to that code, with a fee ceiling the user signs for — see Enrolling a builder-bound key.
Each order states the fee it wants to charge, within that ceiling — see Charging a builder fee.
Note: A builder code is bound to an API key at enrollment and is frozen there — it cannot be attached to an already-enrolled key, and an order never names its own code. This is what stops one integration from attributing another's flow to itself.
Fee units
Builder fees are expressed in hundred-thousandths (per_100k), the unit the on-chain fee schedule uses. 1 = 0.1 basis points (bps; one bps is one hundredth of one percent) = 0.001%.
per_100k
bps
percent
1
0.1
0.001%
10
1
0.01%
100
10
0.1%
The maximum is 100 (0.1%).
Note: This is not the same unit as the market fee rates elsewhere in the API, which are in micros (
10^-6) — see Numeric scaling.1 per_100k=10 micros.
Registering as a builder
Contact Perpl to register. Builder codes are issued by the operator; there is no self-service endpoint. You provide:
Display name
Shown to your users in the wallet prompt when they authorize a key (see What the user signs).
Perpl account address
The Perpl account your accrued builder fees are paid out to.
You receive a builder id in the range 1..255 (the id is a uint8 on-chain, so the registry is deliberately small).
Enrolling a builder-bound key
Enrollment is the same two-step, wallet-authorized flow as an ordinary key (see Programmatic enrollment) — the only difference is two extra fields on the payload request:
Request the payload with those fields set:
Sign and submit exactly as in the programmatic enrollment flow. The enroll response echoes the terms back so you can confirm you registered what you intended:
All failures are 400:
builder_id outside 1..255
builder_id must be in 1..255
max_builder_fee_per_100k above the environment ceiling
max_builder_fee_per_100k must be at most <N>
max_builder_fee_per_100k without a builder_id
max_builder_fee_per_100k requires a builder_id
Non-zero fee ceiling on a read-only key
max_builder_fee_per_100k requires the trade scope
Code not registered / not enabled
builder code <N> is not registered
Builder enrollment not enabled in that environment
builder-bound api keys are not enabled
Note:
max_builder_fee_per_100k: 0with abuilder_idis valid and useful — it gives you attribution without a fee. It is also the only shape aread-scoped builder key can take (such a key can never place an order).
What the user signs
The signer is the end user, not you. You generate the Ed25519 key pair and supply the proof-of-possession; the user's wallet signs the EIP-712 payload. That signature is the fee authorization — there is no separate builder-side approval step — so the payload is written to be legible in the wallet prompt:
the machine-enforced terms are EIP-712 fields (
builderId,maxBuilderFeePer100K), andthe same terms in prose are in the payload's
statementfield, naming your registered builder name and the ceiling as a percentage. This is what the user actually reads before approving.
/api-key/enroll re-derives that statement from the registry and rejects the enrollment if it does not match the one signed. The practical consequence: if your builder name changes between the payload and the enroll call, the enrollment fails — request a fresh payload and have the user sign again.
Users see the same builder terms for every key on their /apikeys page and can revoke any key there at any time — consent is granted once, visibility and revocation are continuous.
Charging a builder fee
Orders are placed over the trading WebSocket as an OrderRequest (mt: 22, see Placing Orders). A builder-bound key adds one field, bf:
Rules:
There is no
builder_idon the request. The code comes from the authenticating key — a client that could name its own code could attribute another builder's flow to itself.The enrolled ceiling is a maximum, not a default. Set
bfon every order you want to charge for. Omitting it is not an error: the order executes, attributed to your code, at zero fee — you simply earn nothing on it.A fee above the ceiling is rejected, not clamped. Silently reducing it would make your accounting disagree with the chain.
The fee applies to the size that opens or increases a position. Closing or reducing fills carry no builder fee.
Builder fees only exist on orders routed through the API. Orders a user sends directly on-chain cannot be attributed to a builder.
A builder-fee rejection arrives as a StatusResponse (mt: 3), and no order update (mt: 24) follows:
error
Condition
code
builder fee not permitted for this api key
bf above the key's ceiling, or any bf on a non-builder key
400
api key lacks trade scope
read-scoped key
403
Reconciling what was charged
Fees are reported gross: the f (fee) amount on orders, fills, and account events is the total the user paid — protocol fee plus builder fee. The builder portion is broken out alongside it, so never add the two together.
Both are omitted when zero, so an ordinary (non-builder) account sees no change. A mis-integration is visible within one fill: flow that reaches you with bf unset shows up as volume with bfa: 0.
Getting paid
Accrued builder fees are collected per builder code and settled to the Perpl account registered with your code, on a periodic epoch schedule.
Related pages
Authentication — the full API-key enrollment flow that builder-bound enrollment extends.
WebSocket API — the
OrderRequest(mt: 22) frame thebffield is set on, and the fill/order/stats updates that carrybfa/tbf.Types & Errors — numeric scaling (micros vs
per_100k) and the shared enums.
Last updated