Quickstart: Place an order with Builder Attribution
Use the Trader’s funded Perps account and owner signer to approve the Builder fee once, then place attributed orders. Approval remains valid across sessions until revoked or replaced. Each Builder is identified by its Perps account address. The Builder and Trader must be different accounts.- TypeScript
- Python
- API
createSecureClient() from @polymarket/client with the Trader’s owner
signer. This example uses Viem. See
Wallet Integrations for other
signer providers. Set builderAddress to the Builder’s EVM account address and
instrumentId to the instrument selected in Prepare a Trade.Create a Secure Client
SecureClient for the Trader. Set POLYMARKET_WALLET_ADDRESS to the
Polymarket wallet that owns the Perps account and PRIVATE_KEY to its owner’s
signing key.import { createSecureClient } from "@polymarket/client";
import { privateKey } from "@polymarket/client/viem";
const client = await createSecureClient({
wallet: process.env.POLYMARKET_WALLET_ADDRESS!,
signer: privateKey(process.env.PRIVATE_KEY!),
});
Open a Trader Session
client.openPerpsSession() with the Builder’s account address:const session = await client.openPerpsSession({
builderAttribution: builderAddress,
});
Approve the Builder Fee
session.approveBuilderFee with an explicit maxFeeRate before the Trader’s
first attributed order. Skip this step if the Trader already has an active approval:await session.approveBuilderFee({ maxFeeRate: "0.001" });
Output: PerpsBuilderApproval
Output: PerpsBuilderApproval
type PerpsBuilderApproval = {
trader: EvmAddress;
builder: EvmAddress;
maxFeeRate: DecimalString; // Zero revokes consent
approvalVersion: number;
timestamp: EpochMilliseconds;
sequence: number;
};
{
"trader": "0x0000000000000000000000000000000000005678",
"builder": "0x0000000000000000000000000000000000001234",
"maxFeeRate": "0.001",
"approvalVersion": 1,
"timestamp": 1767000000000,
"sequence": 1234567889
}
Place an Attributed Order
session.placeOrder() to submit an immediate-or-cancel buy using the
session’s Builder:import { OrderSide, PerpsTimeInForce } from "@polymarket/client";
const result = await session.placeOrder({
instrumentId,
side: OrderSide.BUY,
quantity: "0.01",
timeInForce: PerpsTimeInForce.IOC,
});
Output: PlacePerpsOrderResult
Output: PlacePerpsOrderResult
type PlacePerpsOrderResult = {
order: PerpsOrder;
};
type PerpsOrder = {
id: PerpsOrderId;
instrumentId: PerpsInstrumentId;
side: OrderSide;
price: DecimalString;
quantity: DecimalString;
timeInForce: PerpsTimeInForce;
postOnly: boolean;
reduceOnly: boolean;
status: PerpsOrderStatus;
restingQuantity: DecimalString;
filledQuantity: DecimalString;
createdTimestamp: EpochMilliseconds;
updatedTimestamp: EpochMilliseconds;
clientOrderId: string | undefined;
tpSl: PerpsTpSlOrderFields | undefined;
builder?: PerpsBuilderTerms;
};
type PerpsBuilderTerms = {
address: EvmAddress;
feeRate: DecimalString;
};
{
"order": {
"id": 67890,
"instrumentId": 1,
"side": "BUY",
"price": "65000",
"quantity": "0.01",
"timeInForce": "ioc",
"postOnly": false,
"reduceOnly": false,
"status": "filled",
"restingQuantity": "0",
"filledQuantity": "0.01",
"createdTimestamp": 1767000010000,
"updatedTimestamp": 1767000010000,
"builder": {
"address": "0x0000000000000000000000000000000000001234",
"feeRate": "0.001"
}
}
}
AsyncSecureClient from polymarket-client and run the examples inside an async
function. builder_address is your enabled Builder’s account address.
instrument_id identifies the instrument selected in
Prepare a Trade.Create a Secure Client
AsyncSecureClient for the Trader. Set POLYMARKET_WALLET_ADDRESS
to the Polymarket wallet that owns the Perps account and PRIVATE_KEY to its
owner’s signing key.import os
from polymarket import AsyncSecureClient
client = await AsyncSecureClient.create(
private_key=os.environ["PRIVATE_KEY"],
wallet=os.environ["POLYMARKET_WALLET_ADDRESS"],
)
Open a Trader Session
client.open_perps_session() with the Builder’s account address:session = await client.open_perps_session(builder_attribution=builder_address)
Approve the Builder Fee
session.approve_builder_fee with an explicit max_fee_rate before the Trader’s
first attributed order. Skip this step if the Trader already has an active approval:await session.approve_builder_fee(max_fee_rate="0.001")
Output: PerpsBuilderApproval
Output: PerpsBuilderApproval
class PerpsBuilderApproval:
trader: EvmAddress
builder: EvmAddress
max_fee_rate: Decimal # Zero revokes consent
approval_version: int
timestamp: datetime
sequence: int
{
"trader": "0x0000000000000000000000000000000000005678",
"builder": "0x0000000000000000000000000000000000001234",
"max_fee_rate": "0.001",
"approval_version": 1,
"timestamp": "2025-12-29T09:20:00Z",
"sequence": 1234567889
}
Place an Attributed Order
session.place_order() to submit an immediate-or-cancel buy using the
session’s Builder:result = await session.place_order(
instrument_id=instrument_id,
side="BUY",
quantity="0.01",
time_in_force="ioc",
)
Output: PerpsOrderPlacement
Output: PerpsOrderPlacement
class PerpsOrderPlacement:
order: PerpsOrder
tp_sl: PerpsPlacedTpSlOrders | None = None
class PerpsOrder:
id: PerpsOrderId
instrument_id: PerpsInstrumentId
side: OrderSide
price: Decimal
quantity: Decimal
time_in_force: PerpsTimeInForce
post_only: bool
reduce_only: bool
status: PerpsOrderStatus
resting_quantity: Decimal
filled_quantity: Decimal
created_at: datetime
updated_at: datetime
client_order_id: str | None = None
tp_sl: PerpsTpSlOrderFields | None = None
builder: PerpsBuilderAttribution | None = None
class PerpsBuilderAttribution:
address: EvmAddress
fee_rate: Decimal
{
"order": {
"id": 67890,
"instrument_id": 1,
"side": "BUY",
"price": "65000",
"quantity": "0.01",
"time_in_force": "ioc",
"post_only": false,
"reduce_only": false,
"status": "filled",
"resting_quantity": "0",
"filled_quantity": "0.01",
"builder": {
"address": "0x0000000000000000000000000000000000001234",
"fee_rate": "0.001"
},
"created_at": "2025-12-29T09:20:10Z",
"updated_at": "2025-12-29T09:20:10Z"
}
}
Read the Saved Approval
curl -G "https://api.perpetuals.polymarket.com/v1/account/builder-approvals" \
-H "polymarket-proxy: <proxy_address>" \
-H "polymarket-secret: <proxy_secret>" \
--data-urlencode "builder=<builder_account_address>"
type BuilderApprovals = {
data: Array<{
trader: string;
builder: string;
max_fee_rate: string;
approval_version: number;
timestamp: number;
sequence: number;
}>;
};
{
"data": []
}
data array includes revoked grants. Use the saved
approval_version plus one, or 1 if no grant exists.Build the Approval Operation
const builderAddress = "0x0000000000000000000000000000000000001234";
const builderStatusResponse = await fetch(
`https://api.perpetuals.polymarket.com/v1/info/builder?address=${builderAddress}`,
);
const builderStatus = await builderStatusResponse.json();
const builderFeeRate = builderStatus.max_fee_rate;
Output: BuilderStatus
Output: BuilderStatus
type BuilderStatus = {
address: string;
registered: boolean;
enabled: boolean;
admission_enabled: boolean;
max_fee_rate: string;
};
{
"address": "0x0000000000000000000000000000000000001234",
"registered": true,
"enabled": true,
"admission_enabled": true,
"max_fee_rate": "0.001"
}
builderFeeRate as <builder_fee_rate> below and the saved approval
version plus one as approval_version. This example uses 1 for the first
grant. The account owner signs these terms:{
"type": "approveBuilder",
"args": {
"builder": "0x0000000000000000000000000000000000001234",
"max_fee_rate": "<builder_fee_rate>",
"approval_version": 1
}
}
Create the Approval Hash
keccak256. This example uses @msgpack/msgpack and Viem:import { encode } from "@msgpack/msgpack";
import { keccak256 } from "viem";
const approvalOperation = [
"approveBuilder",
[builderAddress, builderFeeRate, 1],
];
const approvalHash = keccak256(encode(approvalOperation));
Create Approval Typed Data
Op payload using the operation hash:{
"domain": {
"name": "Polymarket",
"version": "1",
"chainId": 137
},
"primaryType": "Op",
"types": {
"Op": [
{
"name": "data",
"type": "bytes32"
},
{
"name": "salt",
"type": "uint64"
},
{
"name": "ts",
"type": "uint64"
}
]
},
"message": {
"data": "<approval_hash>",
"salt": 123456789,
"ts": 1767000000000
}
}
salt with a fresh random integer and ts with the current
Unix timestamp in milliseconds. Use those same values when signing and submitting
the request. The production signing domain uses chainId: 137.Sign Approval Typed Data
OWNER_PRIVATE_KEY to that
key before running the example:import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(
process.env.OWNER_PRIVATE_KEY as `0x${string}`,
);
const approvalSignature = await account.signTypedData({
domain: { name: "Polymarket", version: "1", chainId: 137 },
primaryType: "Op",
types: {
Op: [
{ name: "data", type: "bytes32" },
{ name: "salt", type: "uint64" },
{ name: "ts", type: "uint64" },
],
},
message: {
data: approvalHash,
salt: 123456789n,
ts: 1767000000000n,
},
});
Submit the Approval
approvalSignature as <approval_signature> and the
same salt and timestamp used in the signed typed data:curl -X POST "https://api.perpetuals.polymarket.com/v1/account/builder-approvals" \
-H "Content-Type: application/json" \
-d '{
"op": {
"type": "approveBuilder",
"args": {
"builder": "0x0000000000000000000000000000000000001234",
"max_fee_rate": "<builder_fee_rate>",
"approval_version": 1
}
},
"sig": "<approval_signature>",
"salt": 123456789,
"ts": 1767000000000
}'
type BuilderApproval = {
trader: string;
builder: string;
max_fee_rate: string;
approval_version: number;
timestamp: number;
sequence: number;
};
{
"trader": "0x0000000000000000000000000000000000005678",
"builder": "0x0000000000000000000000000000000000001234",
"max_fee_rate": "0.001",
"approval_version": 1,
"timestamp": 1767000000000,
"sequence": 1234567889
}
approval_version, max_fee_rate, and
sequence. Check that its Builder, rate, and version match the request before
placing an order. If the result is unclear, read the saved grant before signing
a replacement.Build the Order Operation
65000 and the Builder fee
approved above:{
"type": "createOrders",
"args": [
{
"iid": 1,
"buy": true,
"p": "65000",
"qty": "0.01",
"tif": "ioc",
"po": false,
"c": "7f9e4a2b6c8d0e1f1234567890abcdef",
"builder": {
"address": "0x0000000000000000000000000000000000001234",
"fee_rate": "<builder_fee_rate>"
}
}
]
}
Create the Order Hash
import { encode } from "@msgpack/msgpack";
import { keccak256 } from "viem";
const signableOrder = [
"createOrders",
[
[
1,
true,
"65000",
"0.01",
"ioc",
false,
undefined, // ro
"7f9e4a2b6c8d0e1f1234567890abcdef",
undefined, // tr
undefined, // gtt
[builderAddress, builderFeeRate],
].filter((value) => value !== undefined),
],
];
const orderHash = keccak256(encode(signableOrder));
Create Order Typed Data
Op payload using the operation hash:{
"domain": {
"name": "Polymarket",
"version": "1",
"chainId": 137
},
"primaryType": "Op",
"types": {
"Op": [
{
"name": "data",
"type": "bytes32"
},
{
"name": "salt",
"type": "uint64"
},
{
"name": "ts",
"type": "uint64"
}
]
},
"message": {
"data": "<order_hash>",
"salt": 234567890,
"ts": 1767000010000
}
}
salt with a fresh random integer and ts with the current
Unix timestamp in milliseconds. Use those same values when signing and submitting
the request. The production signing domain uses chainId: 137.Sign Order Typed Data
PROXY_PRIVATE_KEY to that
key before running the example:import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(
process.env.PROXY_PRIVATE_KEY as `0x${string}`,
);
const orderSignature = await account.signTypedData({
domain: { name: "Polymarket", version: "1", chainId: 137 },
primaryType: "Op",
types: {
Op: [
{ name: "data", type: "bytes32" },
{ name: "salt", type: "uint64" },
{ name: "ts", type: "uint64" },
],
},
message: {
data: orderHash,
salt: 234567890n,
ts: 1767000010000n,
},
});
Submit the Order
orderSignature as <order_signature>, keeping the operation, salt, and
timestamp identical to the signed values:curl -X POST "https://api.perpetuals.polymarket.com/v1/trade/orders" \
-H "Content-Type: application/json" \
-d '{
"op": {
"type": "createOrders",
"args": [
{
"iid": 1,
"buy": true,
"p": "65000",
"qty": "0.01",
"tif": "ioc",
"po": false,
"c": "7f9e4a2b6c8d0e1f1234567890abcdef",
"builder": {
"address": "0x0000000000000000000000000000000000001234",
"fee_rate": "<builder_fee_rate>"
}
}
]
},
"sig": "<order_signature>",
"salt": 234567890,
"ts": 1767000010000
}'
type OrderResponse = {
status: "ok";
oid: number;
coid?: string;
builder?: {
address: string;
fee_rate: string;
};
} | {
status: "err";
error: string;
oid?: number;
coid?: string;
};
[
{
"status": "ok",
"oid": 67890,
"coid": "7f9e4a2b6c8d0e1f1234567890abcdef",
"builder": {
"address": "0x0000000000000000000000000000000000001234",
"fee_rate": "0.001"
}
}
]
status: "ok" confirms acceptance and
oid identifies the order. status: "err" includes the rejection in error.
Acceptance does not confirm execution.Check the Order State
oid to read the order with the Trader’s proxy credentials:curl -G "https://api.perpetuals.polymarket.com/v1/account/orders" \
-H "polymarket-proxy: <proxy_address>" \
-H "polymarket-secret: <proxy_secret>" \
--data-urlencode "order_id=<oid>"
type OrderData = {
order_id: number;
instrument_id: number;
buy: boolean;
price: string;
quantity: string;
tif: "gtc" | "ioc" | "fok";
post_only: boolean;
ro: boolean;
resting_quantity: string;
filled_quantity: string;
status: string;
created_timestamp: number;
updated_timestamp: number;
client_order_id?: string;
tpsl?: {
kind: "tp" | "sl";
scope: "order" | "position";
trp: string;
parent_oid?: number;
armed_qty?: string;
slip_bps?: number;
};
builder?: {
address: string;
fee_rate: string;
};
};
[
{
"order_id": 67890,
"instrument_id": 1,
"buy": true,
"price": "65000",
"quantity": "0.01",
"tif": "ioc",
"post_only": false,
"ro": false,
"status": "filled",
"resting_quantity": "0",
"filled_quantity": "0.01",
"created_timestamp": 1767000010000,
"updated_timestamp": 1767000010000,
"client_order_id": "7f9e4a2b6c8d0e1f1234567890abcdef",
"builder": {
"address": "0x0000000000000000000000000000000000001234",
"fee_rate": "0.001"
}
}
]
status, filled_quantity, and resting_quantity to determine the
execution outcome. Full order reads include the saved builder terms.
If the snapshot is not available yet, retry the read or follow private order
updates through Authenticated Sessions.Trader Operations
Read and manage the Trader’s consent, place attributed orders, and inspect the fees charged on executions.Fetch Trader Approval
Check whether the Trader has already approved a Builder and the maximum fee they accepted. Read the saved approval before asking for consent again. A revoked approval remains in the account’s history with a zero fee limit.- TypeScript
- Python
- API
session.fetchBuilderApprovals() on the Trader’s existing PerpsSession,
filtering by the Builder’s account address:const approvals = await session.fetchBuilderApprovals({
builder: builderAddress,
});
const approval = approvals[0];
Output: PerpsBuilderApproval[]
Output: PerpsBuilderApproval[]
type PerpsBuilderApproval = {
trader: EvmAddress;
builder: EvmAddress;
maxFeeRate: DecimalString; // Zero revokes consent
approvalVersion: number;
timestamp: EpochMilliseconds;
sequence: number;
};
[
{
"trader": "0x0000000000000000000000000000000000005678",
"builder": "0x0000000000000000000000000000000000001234",
"maxFeeRate": "0.001",
"approvalVersion": 1,
"timestamp": 1767000000000,
"sequence": 1234567889
}
]
approval.maxFeeRate is the approved fee limit as a decimal string.
Zero means consent was revoked. Compare the intended Builder fee with this
limit to decide whether to request approval. approval.approvalVersion is the
saved version, including after revocation.Omit the builder filter to fetch all of the Trader’s saved Builder approvals.session.fetch_builder_approvals() on the Trader’s existing PerpsSession,
filtering by the Builder’s account address:approvals = await session.fetch_builder_approvals(builder=builder_address)
approval = approvals[0] if approvals else None
Output: tuple[PerpsBuilderApproval, …]
Output: tuple[PerpsBuilderApproval, …]
class PerpsBuilderApproval:
trader: EvmAddress
builder: EvmAddress
max_fee_rate: Decimal # Zero revokes consent
approval_version: int
timestamp: datetime
sequence: int
[
{
"trader": "0x0000000000000000000000000000000000005678",
"builder": "0x0000000000000000000000000000000000001234",
"max_fee_rate": "0.001",
"approval_version": 1,
"timestamp": "2025-12-29T09:20:00Z",
"sequence": 1234567889
}
]
approval.max_fee_rate is the approved fee limit as a Decimal.
Zero means consent was revoked. Compare the intended Builder fee with this
limit to decide whether to request approval. approval.approval_version is the
saved version, including after revocation.Omit the builder filter to fetch all of the Trader’s saved Builder approvals.curl -G "https://api.perpetuals.polymarket.com/v1/account/builder-approvals" \
-H "polymarket-proxy: <proxy_address>" \
-H "polymarket-secret: <proxy_secret>" \
--data-urlencode "builder=<builder_account_address>"
type BuilderApprovals = {
data: Array<{
trader: string;
builder: string;
max_fee_rate: string;
approval_version: number;
timestamp: number;
sequence: number;
}>;
};
{
"data": [
{
"trader": "0x0000000000000000000000000000000000005678",
"builder": "0x0000000000000000000000000000000000001234",
"max_fee_rate": "0.001",
"approval_version": 1,
"timestamp": 1767000000000,
"sequence": 1234567889
}
]
}
data array means the Trader has no saved approval for this Builder.
Otherwise, max_fee_rate is the approved fee limit. "0" means consent was
revoked. Compare the intended builder.fee_rate with this limit to decide
whether to request approval. approval_version is the saved version, including
after revocation. Omit the builder query parameter to fetch all saved Builder
approvals for the authenticated Trader.Check whether the Builder can currently receive attributed orders:curl -G "https://api.perpetuals.polymarket.com/v1/info/builder" \
--data-urlencode "address=<builder_account_address>"
type BuilderStatus = {
address: string;
registered: boolean;
enabled: boolean;
admission_enabled: boolean;
max_fee_rate: string;
};
{
"address": "0x0000000000000000000000000000000000001234",
"registered": true,
"enabled": true,
"admission_enabled": true,
"max_fee_rate": "0.001"
}
registered, enabled, and admission_enabled to be true. The order’s
builder.fee_rate must be covered by both the Builder’s current configuration
and the Trader’s positive approval. Fetching these values does not create or
change consent. The server validates them again when it receives an order.Grant Approval
Approve the Builder fee once before placing attributed orders. Existing approval remains valid across sessions until revoked or replaced. To approve a changed Builder fee, grant approval again.- TypeScript
- Python
- API
session.approveBuilderFee on a Trader session opened with
builderAttribution. Pass the maximum fee the Trader agrees to authorize:await session.approveBuilderFee({ maxFeeRate: "0.001" });
maxFeeRate is required and has no default. This example approves 10 basis points
(0.1%). To select a Builder when the session has none, or approve a different
Builder, also pass its address:await session.approveBuilderFee({
builderAddress,
maxFeeRate: "0.001",
});
Output: PerpsBuilderApproval
Output: PerpsBuilderApproval
type PerpsBuilderApproval = {
trader: EvmAddress;
builder: EvmAddress;
maxFeeRate: DecimalString; // Zero revokes consent
approvalVersion: number;
timestamp: EpochMilliseconds;
sequence: number;
};
{
"trader": "0x0000000000000000000000000000000000005678",
"builder": "0x0000000000000000000000000000000000001234",
"maxFeeRate": "0.001",
"approvalVersion": 1,
"timestamp": 1767000000000,
"sequence": 1234567889
}
session.approve_builder_fee on a Trader session opened with
builder_attribution. Pass the maximum fee the Trader agrees to authorize:await session.approve_builder_fee(max_fee_rate="0.001")
max_fee_rate is required and has no default. This example approves 10 basis points
(0.1%). To select a Builder when the session has none, or approve a different
Builder, also pass its address:await session.approve_builder_fee(
builder_address=builder_address,
max_fee_rate="0.001",
)
Output: PerpsBuilderApproval
Output: PerpsBuilderApproval
class PerpsBuilderApproval:
trader: EvmAddress
builder: EvmAddress
max_fee_rate: Decimal # Zero revokes consent
approval_version: int
timestamp: datetime
sequence: int
{
"trader": "0x0000000000000000000000000000000000005678",
"builder": "0x0000000000000000000000000000000000001234",
"max_fee_rate": "0.001",
"approval_version": 1,
"timestamp": "2025-12-29T09:20:00Z",
"sequence": 1234567889
}
approveBuilder operation shown in the approval workflow
above. Read the saved grant first and set approval_version to its current
version plus one, or 1 for the first grant. Sign and submit the same Builder
address, fee, and version.Revoke Approval
Revoke permission for new attributed orders. Existing accepted orders, including pending conditional orders, retain their saved fees. Cancel those orders to prevent further charges.- TypeScript
- Python
- API
session.revokeBuilderFee() with the Builder’s account address:await session.revokeBuilderFee(builderAddress);
Output: PerpsBuilderApproval
Output: PerpsBuilderApproval
type PerpsBuilderApproval = {
trader: EvmAddress;
builder: EvmAddress;
maxFeeRate: DecimalString; // Zero revokes consent
approvalVersion: number;
timestamp: EpochMilliseconds;
sequence: number;
};
{
"trader": "0x0000000000000000000000000000000000005678",
"builder": "0x0000000000000000000000000000000000001234",
"maxFeeRate": "0",
"approvalVersion": 2,
"timestamp": 1767000030000,
"sequence": 1234567891
}
session.revoke_builder_fee() with the Builder’s account address:await session.revoke_builder_fee(builder_address)
Output: PerpsBuilderApproval
Output: PerpsBuilderApproval
class PerpsBuilderApproval:
trader: EvmAddress
builder: EvmAddress
max_fee_rate: Decimal # Zero revokes consent
approval_version: int
timestamp: datetime
sequence: int
{
"trader": "0x0000000000000000000000000000000000005678",
"builder": "0x0000000000000000000000000000000000001234",
"max_fee_rate": "0",
"approval_version": 2,
"timestamp": "2025-12-29T09:20:30Z",
"sequence": 1234567891
}
Read the Current Version
curl -G "https://api.perpetuals.polymarket.com/v1/account/builder-approvals" \
-H "polymarket-proxy: <proxy_address>" \
-H "polymarket-secret: <proxy_secret>" \
--data-urlencode "builder=<builder_account_address>"
type BuilderApprovals = {
data: Array<{
trader: string;
builder: string;
max_fee_rate: string;
approval_version: number;
timestamp: number;
sequence: number;
}>;
};
{
"data": [
{
"trader": "0x0000000000000000000000000000000000005678",
"builder": "0x0000000000000000000000000000000000001234",
"max_fee_rate": "0.001",
"approval_version": 1,
"timestamp": 1767000000000,
"sequence": 1234567889
}
]
}
approval_version plus one in the next step. The example uses 2 for a grant whose current version is 1.Create the Revocation Hash
"0" and use the next saved version. Hash the compact operation:import { encode } from "@msgpack/msgpack";
import { keccak256 } from "viem";
const revocationOperation = [
"approveBuilder",
["0x0000000000000000000000000000000000001234", "0", 2],
];
const revocationHash = keccak256(encode(revocationOperation));
Create Revocation Typed Data
Op structure shown in the approval workflow. Set data
to revocationHash and generate a new random salt and current Unix-millisecond
timestamp. The next examples use sample values that you must replace in both
the signature and the request.Sign Revocation Typed Data
OWNER_PRIVATE_KEY to that
key before running the example:import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(
process.env.OWNER_PRIVATE_KEY as `0x${string}`,
);
const revocationSignature = await account.signTypedData({
domain: { name: "Polymarket", version: "1", chainId: 137 },
primaryType: "Op",
types: {
Op: [
{ name: "data", type: "bytes32" },
{ name: "salt", type: "uint64" },
{ name: "ts", type: "uint64" },
],
},
message: {
data: revocationHash,
salt: 456789012n,
ts: 1767000030000n,
},
});
Submit the Revocation
revocationSignature as <revocation_signature> with the matching
operation, salt, and timestamp:curl -X POST "https://api.perpetuals.polymarket.com/v1/account/builder-approvals" \
-H "Content-Type: application/json" \
-d '{
"op": {
"type": "approveBuilder",
"args": {
"builder": "0x0000000000000000000000000000000000001234",
"max_fee_rate": "0",
"approval_version": 2
}
},
"sig": "<revocation_signature>",
"salt": 456789012,
"ts": 1767000030000
}'
type BuilderApproval = {
trader: string;
builder: string;
max_fee_rate: string;
approval_version: number;
timestamp: number;
sequence: number;
};
{
"trader": "0x0000000000000000000000000000000000005678",
"builder": "0x0000000000000000000000000000000000001234",
"max_fee_rate": "0",
"approval_version": 2,
"timestamp": 1767000030000,
"sequence": 1234567891
}
max_fee_rate: "0" and the version you submitted.
Stale or replayed versions are rejected. If the result is unclear, read the
saved grant before signing another update.Place Orders
The Trader must have granted fee approval for the Builder before submitting attributed orders. The order’s Builder fee must be covered by that approval. These examples place an immediate-or-cancel buy using an existing trading session or signed order.- TypeScript
- Python
- API
session.placeOrder() on the Trader’s existing PerpsSession, opened
with builderAttribution. The session uses the selected Builder and its saved
approved fee. Set instrumentId to the instrument you want to trade:import { OrderSide, PerpsTimeInForce } from "@polymarket/client";
const result = await session.placeOrder({
instrumentId,
side: OrderSide.BUY,
quantity: "0.01",
timeInForce: PerpsTimeInForce.IOC,
});
Output: PlacePerpsOrderResult
Output: PlacePerpsOrderResult
type PlacePerpsOrderResult = {
order: PerpsOrder;
};
type PerpsOrder = {
id: PerpsOrderId;
instrumentId: PerpsInstrumentId;
side: OrderSide;
price: DecimalString;
quantity: DecimalString;
timeInForce: PerpsTimeInForce;
postOnly: boolean;
reduceOnly: boolean;
status: PerpsOrderStatus;
restingQuantity: DecimalString;
filledQuantity: DecimalString;
createdTimestamp: EpochMilliseconds;
updatedTimestamp: EpochMilliseconds;
clientOrderId: string | undefined;
tpSl: PerpsTpSlOrderFields | undefined;
builder?: PerpsBuilderTerms;
};
type PerpsBuilderTerms = {
address: EvmAddress;
feeRate: DecimalString;
};
{
"order": {
"id": 67890,
"instrumentId": 1,
"side": "BUY",
"price": "65000",
"quantity": "0.01",
"timeInForce": "ioc",
"postOnly": false,
"reduceOnly": false,
"status": "filled",
"restingQuantity": "0",
"filledQuantity": "0.01",
"createdTimestamp": 1767000010000,
"updatedTimestamp": 1767000010000,
"builder": {
"address": "0x0000000000000000000000000000000000001234",
"feeRate": "0.001"
}
}
}
session.place_order() on the Trader’s existing PerpsSession, opened
with builder_attribution. The session uses the selected Builder and its saved
approved fee. Set instrument_id to the instrument you want to trade:result = await session.place_order(
instrument_id=instrument_id,
side="BUY",
quantity="0.01",
time_in_force="ioc",
)
Output: PerpsOrderPlacement
Output: PerpsOrderPlacement
class PerpsOrderPlacement:
order: PerpsOrder
tp_sl: PerpsPlacedTpSlOrders | None = None
class PerpsOrder:
id: PerpsOrderId
instrument_id: PerpsInstrumentId
side: OrderSide
price: Decimal
quantity: Decimal
time_in_force: PerpsTimeInForce
post_only: bool
reduce_only: bool
status: PerpsOrderStatus
resting_quantity: Decimal
filled_quantity: Decimal
created_at: datetime
updated_at: datetime
client_order_id: str | None = None
tp_sl: PerpsTpSlOrderFields | None = None
builder: PerpsBuilderAttribution | None = None
class PerpsBuilderAttribution:
address: EvmAddress
fee_rate: Decimal
{
"order": {
"id": 67890,
"instrument_id": 1,
"side": "BUY",
"price": "65000",
"quantity": "0.01",
"time_in_force": "ioc",
"post_only": false,
"reduce_only": false,
"status": "filled",
"resting_quantity": "0",
"filled_quantity": "0.01",
"builder": {
"address": "0x0000000000000000000000000000000000001234",
"fee_rate": "0.001"
},
"created_at": "2025-12-29T09:20:10Z",
"updated_at": "2025-12-29T09:20:10Z"
}
}
createOrders operation with the Builder’s account address and
an approved fee_rate. Use the order hashing and trading-proxy signing steps
from the quickstart above. Replace the sample salt and timestamp with fresh
values, then submit the exact operation, salt, and timestamp that you signed:curl -X POST "https://api.perpetuals.polymarket.com/v1/trade/orders" \
-H "Content-Type: application/json" \
-d '{
"op": {
"type": "createOrders",
"args": [
{
"iid": 1,
"buy": true,
"p": "65000",
"qty": "0.01",
"tif": "ioc",
"po": false,
"c": "7f9e4a2b6c8d0e1f1234567890abcdef",
"builder": {
"address": "0x0000000000000000000000000000000000001234",
"fee_rate": "<builder_fee_rate>"
}
}
]
},
"sig": "<order_signature>",
"salt": 234567890,
"ts": 1767000010000
}'
type OrderResponse = {
status: "ok";
oid: number;
coid?: string;
builder?: {
address: string;
fee_rate: string;
};
} | {
status: "err";
error: string;
oid?: number;
coid?: string;
};
[
{
"status": "ok",
"oid": 67890,
"coid": "7f9e4a2b6c8d0e1f1234567890abcdef",
"builder": {
"address": "0x0000000000000000000000000000000000001234",
"fee_rate": "0.001"
}
}
]
status: "ok" confirms acceptance and
oid identifies the order. status: "err" includes the rejection in error.
Acceptance does not confirm execution. Read the order state or follow private
order updates to determine the execution outcome.Display Trader Fees
Show the total fee charged for each execution, with exchange and Builder fees as the breakdown. A maker rebate can make the total negative. Do not clamp it to zero.- TypeScript
- Python
- API
session.listFills() on the Trader’s PerpsSession:const pages = session.listFills();
const page = await pages.firstPage();
const fills = page.items;
fills has this shape. Use totalFee to display the total charge.Output: PerpsAccountFill
Output: PerpsAccountFill
type PerpsAccountFill = {
tradeId: PerpsTradeId;
orderId: PerpsOrderId;
instrumentId: PerpsInstrumentId;
side: PerpsSide; // long or short
price: DecimalString;
quantity: DecimalString;
taker: boolean;
fee: DecimalString; // Exchange fee; negative for a maker rebate
builderFee: DecimalString; // Non-negative Builder fee
totalFee: DecimalString; // fee + builderFee; may be negative
builder?: PerpsBuilderTerms;
feeAsset: string; // Asset in which all three fees are charged
previousSize: DecimalString;
previousEntryPrice: DecimalString;
pnl: DecimalString;
liquidation: boolean;
timestamp: EpochMilliseconds;
hash: TxHash | undefined;
};
type PerpsBuilderTerms = {
address: EvmAddress;
feeRate: DecimalString;
};
{
"tradeId": 12345,
"orderId": 67890,
"instrumentId": 1,
"side": "long",
"price": "65000",
"quantity": "0.01",
"taker": true,
"fee": "0.26",
"builderFee": "0.65",
"totalFee": "0.91",
"builder": {
"address": "0x0000000000000000000000000000000000001234",
"feeRate": "0.001"
},
"feeAsset": "pUSD",
"previousSize": "0",
"previousEntryPrice": "0",
"pnl": "0",
"liquidation": false,
"timestamp": 1767000010000
}
session.list_fills() on the Trader’s PerpsSession:pages = session.list_fills()
page = await pages.first_page()
fills = page.items
fills has this shape. Use total_fee to display the total charge.Output: PerpsFill
Output: PerpsFill
class PerpsFill:
trade_id: PerpsTradeId
order_id: PerpsOrderId
instrument_id: PerpsInstrumentId
side: PerpsSide # long or short
price: Decimal
quantity: Decimal
taker: bool
fee: Decimal # Exchange fee; negative for a maker rebate
builder_fee: Decimal # Non-negative Builder fee
total_fee: Decimal # fee + builder_fee; may be negative
builder: PerpsBuilderAttribution | None = None
fee_asset: str # Asset in which all three fees are charged
previous_size: Decimal
previous_entry_price: Decimal
pnl: Decimal
liquidation: bool
timestamp: datetime
hash: str | None = None
client_order_id: str | None = None
class PerpsBuilderAttribution:
address: EvmAddress
fee_rate: Decimal
{
"trade_id": 12345,
"order_id": 67890,
"instrument_id": 1,
"side": "long",
"price": "65000",
"quantity": "0.01",
"taker": true,
"fee": "0.26",
"builder_fee": "0.65",
"total_fee": "0.91",
"builder": {
"address": "0x0000000000000000000000000000000000001234",
"fee_rate": "0.001"
},
"fee_asset": "pUSD",
"previous_size": "0",
"previous_entry_price": "0",
"pnl": "0",
"liquidation": false,
"timestamp": "2025-12-29T09:20:10Z",
"hash": null
}
curl "https://api.perpetuals.polymarket.com/v1/account/fills" \
-H "polymarket-proxy: <proxy_address>" \
-H "polymarket-secret: <proxy_secret>"
type AccountTrades = {
data: Array<{
trade_id: number;
order_id: number;
instrument_id: number;
side: "long" | "short";
price: string;
quantity: string;
taker: boolean;
fee: string; // Exchange fee; negative for a maker rebate
fee_asset: string; // Asset in which the fees are charged
previous_size: string;
previous_entry_price: string;
pnl: string;
liquidation: boolean;
adl: boolean;
timestamp: number;
hash: TxHash | undefined;
builder_fee: string; // Non-negative Builder fee
total_fee: string; // fee + builder_fee; may be negative
builder?: {
address: string;
fee_rate: string;
};
}>;
more: boolean;
cursor?: string;
};
{
"data": [
{
"trade_id": 12345,
"order_id": 67890,
"instrument_id": 1,
"side": "long",
"price": "65000",
"quantity": "0.01",
"taker": true,
"fee": "0.26",
"builder_fee": "0.65",
"total_fee": "0.91",
"fee_asset": "pUSD",
"previous_size": "0",
"previous_entry_price": "0",
"pnl": "0",
"timestamp": 1767000010000,
"liquidation": false,
"adl": false,
"hash": "0x",
"builder": {
"address": "0x0000000000000000000000000000000000001234",
"fee_rate": "0.001"
}
}
],
"more": false
}
fea for the fee asset.Rounding and PnL
Builder fees apply to eligible maker and taker executions, including voluntary closes. Forced liquidation legs, backstop transfers, and automatic deleveraging are excluded. An ordinary trading counterparty can still pay its saved fee.| Case | Accounting Rule |
|---|---|
| One execution | Round the Builder fee down to the fee asset’s precision: six decimal places for pUSD. |
| Multiple executions | Add the individually rounded fees. Two fees of 0.0000019 pUSD produce a total charge of 0.000002 pUSD. Discarded fractions do not carry forward. |
| Account net PnL and main PnL leaderboard | Both fee components are already included. Do not subtract them again. |
| Raw fill PnL and biggest-win rankings | Values remain before fees. |
Builder Operations
Check Builder Availability
Check whether a Builder can receive attributed orders.- TypeScript
- Python
- API
client.fetchPerpsBuilderStatus() on an existing SecureClient. Set
builderAddress to the Builder’s Perps account address:const status = await client.fetchPerpsBuilderStatus({
address: builderAddress,
});
if (!status.registered || !status.enabled || !status.admissionEnabled) {
throw new Error("Builder fees are unavailable for this account");
}
Output: PerpsBuilderStatus
Output: PerpsBuilderStatus
type PerpsBuilderStatus = {
address: EvmAddress;
registered: boolean;
enabled: boolean;
admissionEnabled: boolean;
maxFeeRate: DecimalString;
};
{
"address": "0x0000000000000000000000000000000000001234",
"registered": true,
"enabled": true,
"admissionEnabled": true,
"maxFeeRate": "0.001"
}
client.fetch_perps_builder_status() on an existing AsyncSecureClient.
Set builder_address to the Builder’s Perps account address:status = await client.fetch_perps_builder_status(address=builder_address)
if not (status.registered and status.enabled and status.admission_enabled):
raise RuntimeError("Builder fees are unavailable for this account")
Output: PerpsBuilderStatus
Output: PerpsBuilderStatus
class PerpsBuilderStatus:
address: EvmAddress
registered: bool
enabled: bool
admission_enabled: bool
max_fee_rate: Decimal
{
"address": "0x0000000000000000000000000000000000001234",
"registered": true,
"enabled": true,
"admission_enabled": true,
"max_fee_rate": "0.001"
}
curl -G "https://api.perpetuals.polymarket.com/v1/info/builder" \
--data-urlencode "address=<builder_account_address>"
type BuilderStatus = {
address: string;
registered: boolean;
enabled: boolean;
admission_enabled: boolean;
max_fee_rate: string;
};
{
"address": "0x0000000000000000000000000000000000001234",
"registered": true,
"enabled": true,
"admission_enabled": true,
"max_fee_rate": "0.001"
}
registered, enabled, and admission_enabled
are true.Reconcile Builder Earnings
Builders receive their full additional fee immediately in their Perps account. The credit can be used as collateral or withdrawn under the usual account rules. Earnings history reports settled activity. Use the account balance to determine spendable funds. Authenticate as the Builder account for reporting. The Trader’s session used above cannot read another account’s earnings.- TypeScript
- Python
- API
fetchBuilderEarningsSummary() and listBuilderEarnings() on a
PerpsSession opened by the Builder’s SecureClient. Here, builderSession
is that session, created through Authenticated
Sessions.import type { PerpsBuilderEarning } from "@polymarket/client";
const summary = await builderSession.fetchBuilderEarningsSummary();
const pages = builderSession.listBuilderEarnings(summary.snapshot);
const receipts: PerpsBuilderEarning[] = [];
for await (const page of pages) {
receipts.push(...page.items);
}
summary.snapshot fixes start, end, and asOfSequence so every page matches
the summary. To select a window, pass start and end as Unix milliseconds to
fetchBuilderEarningsSummary(). Use earningId to deduplicate receipts.Output: PerpsBuilderEarning
Output: PerpsBuilderEarning
type PerpsBuilderEarning = {
earningId: PerpsBuilderEarningId;
tradeId: PerpsTradeId;
orderId: PerpsOrderId;
instrumentId: PerpsInstrumentId;
trader: EvmAddress; // Main account address
side: OrderSide; // "BUY" or "SELL"
liquidityRole: PerpsLiquidityRole; // "maker" or "taker"
price: DecimalString;
quantity: DecimalString;
clientOrderId?: PerpsClientOrderId;
timestamp: EpochMilliseconds;
sequence: number;
notional: DecimalString;
feeAsset: string;
fee: DecimalString; // Signed exchange fee in feeAsset
builderFee: DecimalString; // Non-negative builder fee in feeAsset
totalFee: DecimalString; // fee + builderFee
feeRate: DecimalString; // Saved builder rate as a decimal fraction
};
{
"earningId": "3:12345:1",
"tradeId": 12345,
"orderId": 67890,
"instrumentId": 3,
"trader": "0x0000000000000000000000000000000000005678",
"side": "BUY",
"liquidityRole": "taker",
"price": "100",
"quantity": "1",
"clientOrderId": "7f9e4a2b6c8d0e1f1234567890abcdef",
"timestamp": 1767000010000,
"sequence": 1234567890,
"notional": "100",
"feeAsset": "pUSD",
"fee": "0.04",
"builderFee": "0.1",
"totalFee": "0.14",
"feeRate": "0.001"
}
Output: PerpsBuilderEarningsSummary
Output: PerpsBuilderEarningsSummary
type PerpsBuilderEarningsSummary = {
assets: PerpsBuilderEarningsAsset[];
traderCount: number; // Distinct traders with attributed fills in the snapshot
activeApprovalCount: number; // Current positive, unrevoked grants, independent of the snapshot
snapshot: PerpsBuilderEarningsSnapshot;
};
type PerpsBuilderEarningsAsset = {
feeAsset: string;
fillCount: number;
notional: DecimalString;
builderFee: DecimalString;
};
type PerpsBuilderEarningsSnapshot = {
start: EpochMilliseconds;
end: EpochMilliseconds;
asOfSequence: number;
};
{
"assets": [
{
"feeAsset": "pUSD",
"fillCount": 1,
"notional": "100",
"builderFee": "0.1"
}
],
"traderCount": 1,
"activeApprovalCount": 2,
"snapshot": {
"start": 1767000000000,
"end": 1767000060000,
"asOfSequence": 1234567890
}
}
fetch_builder_earnings_summary() and list_builder_earnings() on a
PerpsSession opened by the Builder’s AsyncSecureClient. Here,
builder_session is that session, created through Authenticated
Sessions.from polymarket.models.perps import PerpsBuilderEarning
summary = await builder_session.fetch_builder_earnings_summary()
snapshot = summary.snapshot
pages = builder_session.list_builder_earnings(
start=snapshot.start,
end=snapshot.end,
as_of_sequence=snapshot.as_of_sequence,
)
receipts: list[PerpsBuilderEarning] = []
async for page in pages:
receipts.extend(page.items)
summary.snapshot fixes the window and indexed cutoff so every page matches
the summary. To select a window, pass start and end as datetimes or Unix
milliseconds to fetch_builder_earnings_summary(). Use earning_id to
deduplicate receipts. Each page also retains its snapshot, including empty pages.Output: PerpsBuilderEarning
Output: PerpsBuilderEarning
class PerpsBuilderEarning:
earning_id: str
trade_id: PerpsTradeId
order_id: PerpsOrderId
instrument_id: PerpsInstrumentId
trader: EvmAddress # Main account address
side: OrderSide # BUY or SELL
liquidity_role: PerpsLiquidityRole # maker or taker
price: Decimal
quantity: Decimal
client_order_id: str | None = None
timestamp: datetime
sequence: int
notional: Decimal
fee_asset: str
fee: Decimal # Signed exchange fee in fee_asset
builder_fee: Decimal # Non-negative builder fee in fee_asset
total_fee: Decimal # fee + builder_fee
fee_rate: Decimal # Saved builder rate as a decimal fraction
{
"earning_id": "3:12345:1",
"trade_id": 12345,
"order_id": 67890,
"instrument_id": 3,
"trader": "0x0000000000000000000000000000000000005678",
"side": "BUY",
"liquidity_role": "taker",
"price": "100",
"quantity": "1",
"client_order_id": "7f9e4a2b6c8d0e1f1234567890abcdef",
"timestamp": "2025-12-29T09:20:10Z",
"sequence": 1234567890,
"notional": "100",
"fee_asset": "pUSD",
"fee": "0.04",
"builder_fee": "0.1",
"total_fee": "0.14",
"fee_rate": "0.001"
}
Output: PerpsBuilderEarningsSummary
Output: PerpsBuilderEarningsSummary
class PerpsBuilderEarningsSummary:
assets: tuple[PerpsBuilderEarningsAsset, ...]
trader_count: int # Distinct traders with attributed fills in the snapshot
active_approval_count: int # Current positive, unrevoked grants, independent of the snapshot
snapshot: PerpsBuilderEarningsSnapshot
class PerpsBuilderEarningsAsset:
fee_asset: str
fill_count: int
notional: Decimal
builder_fee: Decimal
class PerpsBuilderEarningsSnapshot:
start: datetime
end: datetime
as_of_sequence: int
{
"assets": [
{
"fee_asset": "pUSD",
"fill_count": 1,
"notional": "100",
"builder_fee": "0.1"
}
],
"trader_count": 1,
"active_approval_count": 2,
"snapshot": {
"start": "2025-12-29T09:20:00Z",
"end": "2025-12-29T09:21:00Z",
"as_of_sequence": 1234567890
}
}
curl "https://api.perpetuals.polymarket.com/v1/account/builder-earnings-summary" \
-H "polymarket-proxy: <builder_proxy_address>" \
-H "polymarket-secret: <builder_proxy_secret>"
type BuilderEarningsSummary = {
data: Array<{
fee_asset: string; // Asset in which the fees are charged
fill_count: number;
notional: string;
builder_fee: string; // Non-negative Builder fee
}>;
start_timestamp: number;
end_timestamp: number;
as_of_sequence: number;
trader_count: number;
active_approval_count: number;
};
{
"data": [
{
"fee_asset": "pUSD",
"fill_count": 2,
"notional": "200",
"builder_fee": "0.2"
}
],
"start_timestamp": 1767000000000,
"end_timestamp": 1767000060000,
"as_of_sequence": 1234567891,
"trader_count": 1,
"active_approval_count": 2
}
data, plus start_timestamp, end_timestamp,
as_of_sequence, trader_count, and active_approval_count. Pass the returned
window and cutoff to the receipts request:curl -G "https://api.perpetuals.polymarket.com/v1/account/builder-earnings" \
-H "polymarket-proxy: <builder_proxy_address>" \
-H "polymarket-secret: <builder_proxy_secret>" \
--data-urlencode "start_timestamp=<start_timestamp>" \
--data-urlencode "end_timestamp=<end_timestamp>" \
--data-urlencode "as_of_sequence=<as_of_sequence>"
type BuilderEarnings = {
data: Array<{
earning_id: string;
trade_id: number;
order_id: number;
instrument_id: number;
side: "maker" | "taker";
timestamp: number;
sequence: number;
notional: string;
fee_asset: string; // Asset in which the fees are charged
builder_fee: string; // Non-negative Builder fee
fee_rate: string;
trader: string;
buy: boolean;
price: string;
quantity: string;
client_order_id?: string;
fee: string; // Exchange fee; negative for a maker rebate
total_fee: string; // fee + builder_fee; may be negative
}>;
more: boolean;
cursor?: string;
start_timestamp: number;
end_timestamp: number;
as_of_sequence: number;
};
{
"data": [
{
"earning_id": "3:12345:1",
"trade_id": 12345,
"order_id": 67890,
"instrument_id": 3,
"trader": "0x0000000000000000000000000000000000005678",
"side": "taker",
"buy": true,
"price": "100",
"quantity": "1",
"client_order_id": "7f9e4a2b6c8d0e1f1234567890abcdef",
"timestamp": 1767000010000,
"sequence": 1234567890,
"notional": "100",
"fee_asset": "pUSD",
"fee": "0.04",
"builder_fee": "0.1",
"total_fee": "0.14",
"fee_rate": "0.001"
}
],
"more": true,
"cursor": "<opaque_cursor>",
"start_timestamp": 1767000000000,
"end_timestamp": 1767000060000,
"as_of_sequence": 1234567891
}
cursor while more is true. The cursor preserves the window and
cutoff. Send it unchanged to the same endpoint:curl -G "https://api.perpetuals.polymarket.com/v1/account/builder-earnings" \
-H "polymarket-proxy: <builder_proxy_address>" \
-H "polymarket-secret: <builder_proxy_secret>" \
--data-urlencode "cursor=<cursor>"
type BuilderEarnings = {
data: Array<{
earning_id: string;
trade_id: number;
order_id: number;
instrument_id: number;
side: "maker" | "taker";
timestamp: number;
sequence: number;
notional: string;
fee_asset: string; // Asset in which the fees are charged
builder_fee: string; // Non-negative Builder fee
fee_rate: string;
trader: string;
buy: boolean;
price: string;
quantity: string;
client_order_id?: string;
fee: string; // Exchange fee; negative for a maker rebate
total_fee: string; // fee + builder_fee; may be negative
}>;
more: boolean;
cursor?: string;
start_timestamp: number;
end_timestamp: number;
as_of_sequence: number;
};
{
"data": [
{
"earning_id": "3:12346:1",
"trade_id": 12346,
"order_id": 67891,
"instrument_id": 3,
"trader": "0x0000000000000000000000000000000000005678",
"side": "taker",
"buy": true,
"price": "100",
"quantity": "1",
"client_order_id": "8f9e4a2b6c8d0e1f1234567890abcdef",
"timestamp": 1767000020000,
"sequence": 1234567891,
"notional": "100",
"fee_asset": "pUSD",
"fee": "0.04",
"builder_fee": "0.1",
"total_fee": "0.14",
"fee_rate": "0.001"
}
],
"more": false,
"start_timestamp": 1767000000000,
"end_timestamp": 1767000060000,
"as_of_sequence": 1234567891
}
earning_id to deduplicate receipts.trader is the main account address, not its trading proxy. side identifies
maker or taker. buy identifies direction. Receipts also carry order, trade and
instrument IDs, fee_rate, fee_asset, timestamp, and sequence.
client_order_id is present when the order supplied one.The summary’s data contains totals by fee asset. trader_count counts distinct
accounts with attributed fills in the window and cutoff. active_approval_count
counts current positive, unrevoked grants independently of that window.
Timestamps use Unix milliseconds.Reporting Rules
| Rule | Effect |
|---|---|
| History window | The default is seven days. Each request supports up to 90 days. Select earlier windows for older earnings. |
| Indexed cutoff | Reports include a complete indexed history prefix. They can lag live balance updates. |
| Receipt identity | Each attributed fill side has a receipt, even when its fee rounds to zero. Two sides paying the same Builder produce separate receipts. |
| Summary counts | Volume and fill counts include zero-fee receipts. Distinct Traders count once across markets, assets, and fill sides. |
| Active approvals | Disabling a Builder does not erase its existing grants. |
Follow Live Builder Fills
Use live receipts to update earnings as trades execute. Authenticate as the Builder account, as in the reporting examples above. The feed covers all Traders and markets attributed to that account.- TypeScript
- Python
- API
includeBuilderFills: true on the Builder’s
SecureClient. Here, builderClient authenticates the Builder account through
Authenticated Sessions.const builderSession = await builderClient.openPerpsSession({
includeBuilderFills: true,
});
try {
for await (const event of builderSession) {
if (event.type === "builderFill") {
const receipts = event.payload;
// Persist receipts, deduplicating by earningId.
} else if (event.type === "resync") {
// Reconcile earnings using the recovery workflow below.
}
// Handle other account events here.
}
} finally {
await builderSession.close();
}
builderFill is part of PerpsSessionEvent, alongside orders, fills, balances,
and resync events. There is one session iterator and one connection lifecycle.
includeBuilderFills defaults to false. Setting builderAttribution does
not select whose receipts are delivered. Receipts belong to the authenticated
Builder account.Output: PerpsBuilderFillUpdateEvent
Output: PerpsBuilderFillUpdateEvent
type PerpsBuilderFillUpdateEvent = {
type: "builderFill";
channel: "builderFills";
timestamp: EpochMilliseconds;
sequence: number;
payload: PerpsBuilderEarning[];
};
{
"type": "builderFill",
"channel": "builderFills",
"timestamp": 1767000010000,
"sequence": 1234567890,
"payload": [
{
"earningId": "3:12345:1",
"tradeId": 12345,
"orderId": 67890,
"instrumentId": 3,
"trader": "0x0000000000000000000000000000000000005678",
"side": "BUY",
"liquidityRole": "taker",
"price": "100",
"quantity": "1",
"clientOrderId": "7f9e4a2b6c8d0e1f1234567890abcdef",
"timestamp": 1767000010000,
"sequence": 1234567890,
"notional": "100",
"feeAsset": "pUSD",
"fee": "0.04",
"builderFee": "0.1",
"totalFee": "0.14",
"feeRate": "0.001"
}
]
}
include_builder_fills=True on the Builder’s
AsyncSecureClient. Here, builder_client authenticates the Builder account
through Authenticated Sessions.async with await builder_client.open_perps_session(include_builder_fills=True) as builder_session:
async for event in builder_session:
if event.type == "builder_fill":
receipts = event.payload
# Persist receipts, deduplicating by earning_id.
elif event.type == "resync":
# Reconcile earnings using the recovery workflow below.
pass
# Handle other account events here.
builder_fill is part of PerpsSessionEvent, alongside orders, fills,
balances, and resync events. There is one session iterator and one connection
lifecycle. include_builder_fills defaults to False. Setting
builder_attribution does not select whose receipts are delivered. Receipts
belong to the authenticated Builder account.Output: PerpsBuilderFillEvent
Output: PerpsBuilderFillEvent
class PerpsBuilderFillEvent:
type: Literal["builder_fill"] = "builder_fill"
channel: Literal["builderFills"] = "builderFills"
timestamp: datetime
sequence: int
payload: tuple[PerpsBuilderEarning, ...]
{
"type": "builder_fill",
"channel": "builderFills",
"timestamp": "2025-12-29T09:20:10Z",
"sequence": 1234567890,
"payload": [
{
"earning_id": "3:12345:1",
"trade_id": 12345,
"order_id": 67890,
"instrument_id": 3,
"trader": "0x0000000000000000000000000000000000005678",
"side": "BUY",
"liquidity_role": "taker",
"price": "100",
"quantity": "1",
"client_order_id": "7f9e4a2b6c8d0e1f1234567890abcdef",
"timestamp": "2025-12-29T09:20:10Z",
"sequence": 1234567890,
"notional": "100",
"fee_asset": "pUSD",
"fee": "0.04",
"builder_fee": "0.1",
"total_fee": "0.14",
"fee_rate": "0.001"
}
]
}
Authenticate the Connection
wss://ws.perpetuals.polymarket.com/v1/ws.
Send the Builder’s proxy address and secret in an authentication frame:{
"id": 1,
"req": "post",
"op": {
"type": "auth",
"args": {
"proxy": "<builder_proxy_address>",
"secret": "<builder_proxy_secret>"
}
}
}
type AuthResponse = {
id?: number;
data: {
status: "ok";
pod?: string;
} | {
status: "err";
error: string;
};
};
{
"id": 1,
"data": {
"status": "ok"
}
}
data.status: "ok" before subscribing.
See Session Lifecycle for
connection keepalive and authentication errors.Subscribe to Receipts
{
"id": 2,
"req": "sub",
"chs": [
"builderFills"
]
}
type BuilderFillsSubscribeResponse = {
id?: number;
data: Array<{
status: "ok";
} | {
status: "err";
error: string;
arts?: number;
ts?: number;
ref?: string;
}>;
};
{
"id": 2,
"data": [
{
"status": "ok"
}
]
}
data array with the same receipt fields and earning_id
values as history, including zero-fee fills. The channel name cannot select
another Builder’s account. See Builder Fills
for the message schema. After a disconnect, authenticate and subscribe again.Output: BuilderFillsUpdate
Output: BuilderFillsUpdate
type BuilderFillsUpdate = {
ch: string;
ts: number;
ets: number;
sq: number;
data: Array<{
earning_id: string;
trade_id: number;
order_id: number;
instrument_id: number;
trader: string;
buy: boolean;
price: string;
quantity: string;
client_order_id?: string;
side: "maker" | "taker";
timestamp: number;
sequence: number;
notional: string;
fee_asset: string; // Asset in which the fees are charged
fee: string; // Exchange fee; negative for a maker rebate
builder_fee: string; // Non-negative Builder fee
total_fee: string; // fee + builder_fee; may be negative
fee_rate: string;
}>;
};
{
"ch": "builderFills",
"ts": 1767225600000,
"ets": 1767225600000,
"sq": 1234567890,
"data": [
{
"earning_id": "1:1:0",
"trade_id": 1,
"order_id": 1234567890,
"instrument_id": 1,
"trader": "0x1234567890abcdef1234567890abcdef12345678",
"buy": true,
"price": "100.00",
"quantity": "10.00",
"client_order_id": "550e8400e29b41d4a716446655440000",
"side": "maker",
"sequence": 1234567890,
"notional": "1000.00",
"timestamp": 1767225600000,
"fee_asset": "pUSD",
"fee": "0.125",
"builder_fee": "1.00",
"total_fee": "1.125",
"fee_rate": "0.001"
}
]
}
Recover Missed Receipts
The feed has no initial history snapshot or exactly-once delivery guarantee. Its sequence numbers can skip values because other account activity shares the sequence. A gap alone does not prove receipts were lost. Use history to recover missed receipts after connecting or reconnecting:Buffer Live Receipts
Read Overlapping History
Merge Receipts
Check Indexing Progress
Handle Order Errors
If an order is rejected because the Trader has not approved the Builder fee, request approval for that fee before placing another order. The examples below approve 10 basis points (0.1%) after the Trader accepts the fee.- TypeScript
- Python
- API
session.placeOrder() throws RequestRejectedError with the message
builder_approval_required when approval is missing, revoked, or does not cover
the order’s Builder fee. Check error.message and call approveBuilderFee
with the Builder address and the agreed rate:import { OrderSide, PerpsTimeInForce, RequestRejectedError } from "@polymarket/client";
try {
await session.placeOrder({
instrumentId,
side: OrderSide.BUY,
quantity: "0.01",
timeInForce: PerpsTimeInForce.IOC,
});
} catch (error) {
if (
!(error instanceof RequestRejectedError) ||
error.message !== "builder_approval_required"
) {
throw error;
}
await session.approveBuilderFee({
builderAddress,
maxFeeRate: "0.001", // 10 bps
});
}
Output: PerpsBuilderApproval
Output: PerpsBuilderApproval
type PerpsBuilderApproval = {
trader: EvmAddress;
builder: EvmAddress;
maxFeeRate: DecimalString; // Zero revokes consent
approvalVersion: number;
timestamp: EpochMilliseconds;
sequence: number;
};
{
"trader": "0x0000000000000000000000000000000000005678",
"builder": "0x0000000000000000000000000000000000001234",
"maxFeeRate": "0.001",
"approvalVersion": 1,
"timestamp": 1767000000000,
"sequence": 1234567889
}
session.postOrders(), inspect
each result’s status and error for the same rejection.session.place_order() raises RequestRejectedError with the message
builder_approval_required when approval is missing, revoked, or does not cover
the order’s Builder fee. Check str(error) and call approve_builder_fee
with the Builder address and the agreed rate:from polymarket.errors import RequestRejectedError
try:
await session.place_order(
instrument_id=instrument_id,
side="BUY",
quantity="0.01",
time_in_force="ioc",
)
except RequestRejectedError as error:
if str(error) != "builder_approval_required":
raise
await session.approve_builder_fee(
builder_address=builder_address,
max_fee_rate="0.001", # 10 bps
)
Output: PerpsBuilderApproval
Output: PerpsBuilderApproval
class PerpsBuilderApproval:
trader: EvmAddress
builder: EvmAddress
max_fee_rate: Decimal # Zero revokes consent
approval_version: int
timestamp: datetime
sequence: int
{
"trader": "0x0000000000000000000000000000000000005678",
"builder": "0x0000000000000000000000000000000000001234",
"max_fee_rate": "0.001",
"approval_version": 1,
"timestamp": "2025-12-29T09:20:00Z",
"sequence": 1234567889
}
session.post_orders(), inspect
each result’s status and error for the same rejection.status: "err" and
error: "builder_approval_required":Output: OrderResponse
Output: OrderResponse
type OrderResponse = {
status: "ok";
oid: number;
coid?: string;
builder?: {
address: string;
fee_rate: string;
};
} | {
status: "err";
error: string;
oid?: number;
coid?: string;
};
[
{
"status": "err",
"error": "builder_approval_required"
}
]
approveBuilder operation in the Grant Approval
workflow with the Builder address, the next approval version, and the agreed
max_fee_rate. For 10 basis points, use "0.001". Submit a new order after the
approval succeeds.