Documentation Index
Fetch the complete documentation index at: https://polynode.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The Fee Escrow lets platforms charge a fee on trades placed through the polynode SDK. Fees are optional, per-order, and fully on-chain. The system uses an escrow model: fees are pulled before order placement and either distributed on fill or refunded on cancel. Users never lose fees on unfilled orders. Key properties:- Fees are opt-in. Set
feeBps: 0to skip the escrow entirely. - Each order gets its own escrow entry with an independent affiliate and split.
- If the operator doesn’t settle within 72 hours, the user can self-refund on-chain.
- All escrow operations are gasless via Polymarket’s relayer.
This is your platform’s fee — separate from anything Polymarket charges.A user trading through your SDK may pay three distinct fees, each collected by a different system:
All three are independent.
| Fee | Who charges it | Where it’s set | How it’s collected |
|---|---|---|---|
| Polymarket protocol fee | Polymarket | V1 Order struct feeRateBps field. V2 has no per-order fee slot. | Taken by the CLOB at settlement, goes to Polymarket’s treasury |
| Polymarket builder rev share (V2 only) | Polymarket | V2 Order struct builder bytes32 field | Polymarket reads the field from the OrderFilled event and pays the builder rev share off-chain |
| Your platform fee (this guide) | Your platform | feeConfig.feeBps in the polynode SDK | Pulled from the user’s collateral into our FeeEscrow contract before the order reaches the CLOB |
feeBps: 0 turns off your platform fee only — it does not waive Polymarket’s protocol fee or builder rev share. Setting your feeBps does not change anything inside the signed CLOB Order struct.Frequently Asked Questions
How does the contract know how much to distribute?
How does the contract know how much to distribute?
It doesn’t know anything about the Polymarket order. The amount is locked at deposit time. When
pullFee is called, the contract stores escrows[orderId].amount = feeAmount. That number is immutable. distribute sends from that stored amount. refund sends back whatever’s left. The contract just moves money between three fixed addresses: the escrow pool, the affiliate wallet, and the treasury.What stops someone else from taking my fee out of the contract?
What stops someone else from taking my fee out of the contract?
Three access controls, each with a single hardcoded destination:
distribute()requiresonlyAuthorized(our operator wallet or contract owner). Sends funds only to theaffiliateandtreasuryaddresses stored at deposit time. Anyone else calling it getsUnauthorized.refund()also requiresonlyAuthorized. Sends funds only back to the originalpayer.claimRefund()can be called by anyone, but the contract checksmsg.sender == escrow.payerandblock.timestamp > pulledAt + 72 hours. Only the original payer can claim, and only after 72h. Funds go exclusively toescrow.payer.
Where exactly does the money go when distributed?
Where exactly does the money go when distributed?
The contract splits every distribution into two transfers:
affiliateShareBps / 10000of the amount goes to theaffiliateaddress (your wallet).- The remainder goes to the
treasuryaddress (set by the contract owner).
affiliateShareBps is 10000 (the default), you keep 100% and nothing goes to the treasury. Both addresses are immutable for each escrow entry.What if the order never fills? How does the fee get returned?
What if the order never fills? How does the fee get returned?
Three independent paths back to the user, any one of which is sufficient:
- Inline refund on cancel. When you call
cancelOrder()orcancelAll(), our cosigner automatically callsrefund()in the same request. The collateral returns to the user’s wallet immediately. - Lifecycle refund. Our background service checks pending escrow orders every 30 seconds using on-chain verification. If an order has no fills after 72 hours, it calls
refund()automatically. - User self-refund. After 72 hours, the user (or anyone on their behalf) can call
claimRefund(orderId)directly on the contract. No involvement from us needed. Funds go to the original payer, always.
Can money get permanently stuck in the contract?
Can money get permanently stuck in the contract?
No. Every escrowed fee has exactly two possible exits, and at least one is always available:
- Distribute sends it to the affiliate and treasury.
- Refund / claimRefund sends it back to the payer.
emergencyWithdraw as a last resort. And claimRefund is always available to the payer after 72 hours with zero dependencies on our infrastructure.Is there any edge case where a cancelled order doesn't get refunded?
Is there any edge case where a cancelled order doesn't get refunded?
The only scenario where a refund doesn’t happen automatically: our cosigner is down at the moment of cancellation AND the lifecycle service is also down AND the user doesn’t call
claimRefund themselves after 72 hours. In that case the funds sit in the contract, untouched, until someone acts. They don’t disappear, they don’t move, and no one else can take them. The moment any one of those three systems comes back online, the refund happens.How does partial fill distribution work?
How does partial fill distribution work?
Our lifecycle service checks on-chain
OrderFilled events every 30 seconds. Each partial fill emits an event with the exact amount filled. The service sums all fills, computes the fill fraction against the original order size, and distributes only the new portion since the last check. The contract tracks cumulative distributed amounts internally and caps at the total, so there’s no risk of over-distribution even if our math has a rounding error. Undistributed remainder is refunded after the 72-hour timeout.How does the system verify that an order actually filled?
How does the system verify that an order actually filled?
Pure on-chain verification. The CLOB order ID is the EIP-712 hash of the signed order, and that same hash appears as an indexed topic in every on-chain
OrderFilled event. Our lifecycle queries eth_getLogs with that hash across all Polymarket V2 exchange contracts. If the event exists on the blockchain, the fill happened. If it doesn’t exist, it didn’t. No API calls, no authentication, no trust assumptions. Immutable blockchain state is the source of truth.V1 and V2 — which one do I use?
Two contracts are live. Both are fully supported; neither is being deprecated.| You’re trading on… | Use | Collateral token |
|---|---|---|
V1 CLOB (clob.polymarket.com) | FeeEscrow V1 0xa11D28433B79D0A88F3119b16A090075752258EA | USDC.e |
V2 CLOB (clob-v2.polymarket.com) | FeeEscrow V2 0x3A43D88ef8Aae4dF5a50B3abf67122CAAeEF7c9F | pUSD |
How It Works
Contract addresses
Both contracts are on Polygon mainnet (chain ID 137), both use the same 72-hour self-refund window, and both have the sameFEE_AUTH_TYPEHASH.
V1 — USDC.e collateral (V1 CLOB)
| Field | Value |
|---|---|
| Address | 0xa11D28433B79D0A88F3119b16A090075752258EA |
| Collateral | USDC.e 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 (6 decimals) |
| EIP-712 domain | name="PolyNodeFeeEscrow", version="1", chainId 137 |
V2 — pUSD collateral (V2 CLOB, live 2026-04-24)
| Field | Value |
|---|---|
| Address | 0x3A43D88ef8Aae4dF5a50B3abf67122CAAeEF7c9F |
| Collateral | pUSD 0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB (6 decimals) |
| EIP-712 domain | name="PolyNodeFeeEscrowV2", version="2", chainId 137 |
Setup
Users need one additional ERC-20 approval for the escrow contract — USDC.e for V1, pUSD for V2. This is added to the existing approval batch during wallet setup (7th approval, gasless, batched with the 6 Polymarket approvals).exchangeVersion: "v2" in your TraderConfig (TypeScript), exchange_version=ExchangeVersion.V2 (Python), or exchange_version: ExchangeVersion::V2 (Rust). ensureReady() then approves pUSD for the V2 FeeEscrow contract as part of the same gasless Safe approval batch as V1 does for USDC.e.
Supported from SDK versions: [email protected] (TypeScript), polynode==0.9.2 (Python), polynode = "0.12.2" (Rust).
Fee Authorization (EIP-712)
The user signs a typed message authorizing a specific fee amount for a specific order. This prevents anyone from pulling more than the user agreed to. TheFeeAuth struct is identical across V1 and V2 — only the domain changes.
Selecting V2 at submit time
The polynode cosigner routes every fee request to V1 by default. To use V2, include the contract address in thefee_auth body of your /submit call:
escrow_contract (or set it to the V1 address) to use V1. Any other value falls through to V1, where the signature check will fail because the V2 domain separator won’t match — you’ll get an InvalidSignature revert rather than funds going to the wrong place.
Order Types
Every order type works with the escrow. The fee is pulled before submission and settled based on the order outcome.Limit Buy (GTC, resting)
Order rests on the book. Fills at any point in the future distribute the fee normally. If cancelled before fill, the fee is refunded. Orders resting past 72 hours are covered explicitly in the 72-Hour Safety Net section below.0x39cb2d... placed at 20c, cancelled, fee refunded. Safe net change: $0.00.
Market Buy (fills immediately)
Order fills instantly. Fee is distributed to treasury.Limit Sell (GTC, resting)
Same flow as limit buy. Rests on book, refunded on cancel.0x71f943... placed at 45c, cancelled, fee refunded.
Market Sell (fills immediately)
Batch Cancel
Multiple resting orders can be cancelled at once. Each gets its own refund.cancelAll(), both fees refunded.
No Fee (Opt-Out)
IffeeBps is 0, the SDK skips the escrow entirely. The order goes straight to the CLOB with no fee interaction. Existing behavior is completely unchanged.
Affiliate Revenue Sharing
Each order can specify an affiliate address and their share of the fee. The split is set per-order, so different partners can have different rates.| Split | Treasury | Affiliate |
|---|---|---|
affiliateShareBps: 10000 (default — you keep 100%) | $0.00 | $0.10 |
affiliateShareBps: 7000 (share 30% with polynode) | $0.03 | $0.07 |
affiliateShareBps: 5000 (50/50 split) | $0.05 | $0.05 |
Partial Fills
If an order partially fills, the fee is proportionally distributed. The remainder stays in escrow until the order fully fills or is cancelled.72-Hour Safety Net
The escrow contract has a 72-hour timeout. After that window, the user can always get their undistributed fee back.- The transfer target is hardcoded.
claimRefundalways sends funds back to the original payer, no matter who calls it. Nobody can steal anyone else’s fee. The reason anyone can trigger it (not just the payer) is purely so a bot or a friend can help a user recover funds if the user is offline. - This is a fail-safe, not the expected path. It exists so that if our backend goes down, gets paused, or disappears entirely, users are guaranteed to get their escrowed money back without needing us. Most users will never know the function exists, and most will never call it.
- It does not fire automatically. The timeout doesn’t trigger anything on its own — it just unlocks the option for the user (or anyone acting on their behalf) to reclaim the fee. If nobody calls
claimRefund, the escrow sits untouched anddistribute/refundcontinue to work normally, forever.
What this means for limit orders resting on the book
This is the scenario to understand clearly. A limit order can rest on the CLOB for as long as needed. The 72-hour timeout starts when the fee is pulled, not when the order fills. Here is exactly what happens at each boundary:| Situation | Behavior |
|---|---|
| Order fills within 72h | Operator calls distribute — funds flow to treasury and affiliate normally. Nothing unusual. |
| Order cancelled within 72h | Operator calls refund — full fee returns to the user. Nothing unusual. |
| Order still resting at hour 72, user does nothing | Escrow sits untouched. When the order eventually fills — minute 73, day 5, month 2 — operator calls distribute and funds flow normally. Everyone is paid. |
Order still resting at hour 72, user calls claimRefund | User gets their fee back. If the order then fills later, the fill is effectively fee-free. The builder gives up the fee on that one order; nothing else breaks. |
claimRefund while their own order is still trying to fill. They’d have no reason to — they placed the order because they wanted the trade. The timeout exists as a safety valve for the case where the operator is unable to settle (outage, pause, shutdown), not as a routine workflow step.
Practical guidance for builders
- If you charge fees on orders expected to fill quickly (aggressive limits near midprice), the 72h window is a non-issue.
- If you charge fees on long-dated limits that commonly rest for days, you can either accept that a fully-informed user has an escape hatch, or skip the fee on those specific orders by passing
feeBps: 0. - If you really want to keep fee coverage alive on a long-resting order, the operator can proactively
refundbefore hour 72 and re-pull a fresh signature from the user with a new nonce. This is a manual flow and most builders won’t need it.
Security
V1 has been through three independent security audits and 57 Foundry tests covering unit, fuzz, and adversarial scenarios. V2 is a strict two-change diff from V1 (collateral token address + EIP-712 domain strings). Every other line of the contract is byte-identical to the audited V1 source. Verification against the deployed V2 contract on Polygon mainnet: 8/8 fork tests pass (pullFee, distribute 30/70 split, refund, 72h claimRefund, and every revert path — expired deadline, wrong nonce, tampered signature). Tests impersonate a real on-chain pUSD holder to fund the payer. Both versions enforce the same invariants:- Signature malleability — rejected (secp256k1 s-value upper bound check)
- Replay attacks — prevented by per-signer sequential nonces
- Affiliate share overflow — capped at 10000 bps (100%)
- Self-referential payer — blocked (payer cannot be the escrow contract)
- ecrecover(0) — rejected explicitly
- Operator compromise — operator can be revoked by Safe owner instantly
- Stuck funds — 72-hour user self-refund + owner emergency withdraw
SDK Integration
Enable fee collection with a single config option. Everything else is automatic.Basic Setup
affiliate field is required when feeBps > 0. This is the wallet where your fees are sent. By default, you keep 100% of the fee. If you want to share a portion with the polynode treasury, set affiliateShareBps to less than 10000:
feeConfig is set with feeBps > 0, every order automatically:
- Calculates the fee from the order’s price and size
- Signs an EIP-712 authorization (free, off-chain)
- Pulls the fee into escrow before the order reaches the CLOB
- Refunds the fee automatically if the order is cancelled
Fee Calculation
The fee is calculated as:feeBps: 50:
Placing an Order with Fees
feeEscrowTxHash (the on-chain transaction that moved USDC into escrow) and feeAmount (how much was charged).
Cancelling (Auto-Refund)
cancelAll() also triggers refunds for every cancelled order that had a fee.
Per-Order Fee Override
Override the globalfeeConfig on individual orders:
Opting Out
SetfeeBps: 0 or omit feeConfig entirely. The SDK skips the escrow and the order goes straight to the CLOB with zero overhead. Existing behavior is completely unchanged.
What the SDK Does Under the Hood
When you calltrader.order() with feeBps > 0:
Approvals
The escrow contract requires one USDC approval. This is automatically included in the approval batch duringensureReady() — the 7th approval alongside the 6 Polymarket approvals. For Safe wallets, it’s batched into the same multicall transaction (no extra gas). For EOA wallets, it’s one additional approval TX (~$0.001 MATIC).
