Skip to main content

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: 0 to 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.

How It Works

1. User signs EIP-712 fee authorization (off-chain, free)
2. Operator calls pullFee → USDC moves from user's Safe to escrow
3. Order goes to Polymarket CLOB (unchanged, builder creds preserved)
4. On fill   → operator calls distribute → fee splits to treasury + affiliate
   On cancel → operator calls refund    → USDC returns to user's Safe
   On timeout (72h) → user calls claimRefund → USDC returns automatically
The fee escrow is completely independent from the Polymarket order flow. Builder credentials, order signing, and CLOB submission are unchanged. The fee is a separate USDC transfer that happens before the order.

Contract

FieldValue
Address0xa11D28433B79D0A88F3119b16A090075752258EA
ChainPolygon (137)
USDC0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 (USDC.e)
Timeout72 hours (user self-refund after this)

Setup

Users need one additional USDC approval for the escrow contract. This is added to the existing approval batch during wallet setup (7th approval, gasless, batched with the 6 Polymarket approvals).
// Already handled by ensureReady() — no manual setup needed.
// The SDK adds the escrow approval to the existing Safe setup batch.
const status = await trader.ensureReady(privateKey);

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.
// EIP-712 domain
{
  name: "PolyNodeFeeEscrow",
  version: "1",
  chainId: 137,
  verifyingContract: "0xa11D28433B79D0A88F3119b16A090075752258EA"
}

// FeeAuth struct
{
  orderId: bytes32,   // unique order identifier
  payer: address,     // user's Safe wallet (where USDC is pulled from)
  signer: address,    // user's EOA (signs this message)
  feeAmount: uint256, // fee in USDC (6 decimals)
  deadline: uint256,  // unix timestamp — authorization expires after this
  nonce: uint256      // signer's current nonce (prevents replay)
}

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. If cancelled before fill, fee is refunded.
pullFee($0.005)  →  order BUY 5 @ $0.20  →  cancel  →  refund($0.005)
                                                         ↳ user gets fee back
Verified on Polygon — order 0x39cb2d... placed at 20c, cancelled, fee refunded. Safe net change: $0.00.

Market Buy (fills immediately)

Order fills instantly. Fee is distributed to treasury.
pullFee($0.005)  →  order BUY 5 @ $0.36  →  fills  →  distribute($0.005)
                                              ↳ 5 shares for $1.80    ↳ treasury receives fee
Verified on Polygon — matched, making=$1.80 taking=5 shares. Fee distributed to treasury.

Limit Sell (GTC, resting)

Same flow as limit buy. Rests on book, refunded on cancel.
pullFee($0.005)  →  order SELL 5 @ $0.45  →  cancel  →  refund($0.005)
Verified on Polygon — order 0x71f943... placed at 45c, cancelled, fee refunded.

Market Sell (fills immediately)

pullFee($0.005)  →  order SELL 5 @ $0.35  →  fills  →  distribute($0.005)
Verified on Polygon — matched, fee distributed.

Batch Cancel

Multiple resting orders can be cancelled at once. Each gets its own refund.
pullFee(order_a)  →  place order A
pullFee(order_b)  →  place order B
                     cancelAll()
                     refund(order_a)
                     refund(order_b)
Verified on Polygon — 2 orders cancelled via cancelAll(), both fees refunded.

No Fee (Opt-Out)

If feeBps 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.
// Example: partner keeps 90% of a $0.10 fee
pullFee({
  orderId: "0x...",
  feeAmount: 100000,    // $0.10
  affiliate: "0xPartnerWallet...",
  affShareBps: 9000,    // 90% to partner
})

// On distribute:
//   Partner receives: $0.090
//   Treasury receives: $0.010
SplitTreasuryAffiliate
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
All splits verified on Polygon mainnet with real USDC transfers.

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.
pullFee($0.20)
  → distribute(30%)  → $0.06 to treasury
  → distribute(40%)  → $0.08 to treasury
  → refund(rest)     → $0.06 back to user
Verified on Polygon — partial fills distribute proportionally, refund returns the exact remainder.

72-Hour Safety Net

If the operator hasn’t settled an order within 72 hours, anyone can call claimRefund(orderId) to return the undistributed fee to the user. This is permissionless and requires no API key or operator involvement.
// After 72 hours, anyone can call this
escrow.claimRefund(orderId);
// → undistributed USDC returns to the payer's Safe
This protects users if our backend goes down. Funds are never permanently locked.

Security

The contract has been through three independent security audits:
  • 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
57 Foundry tests covering unit tests, fuzz tests, and adversarial attack scenarios.

SDK Integration

Enable fee collection with a single config option. Everything else is automatic.

Basic Setup

import { PolyNodeTrader } from 'polynode-sdk';

const trader = new PolyNodeTrader({
  polynodeKey: 'pn_live_...',
  feeConfig: {
    feeBps: 50,                          // 0.5% fee on every order
    affiliate: '0xYourWallet...',        // REQUIRED: your wallet that receives the fee
  },
});
The 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: {
  feeBps: 50,
  affiliate: '0xYourWallet...',
  affiliateShareBps: 7000,              // optional: keep 70%, share 30% with polynode
}
When feeConfig is set with feeBps > 0, every order automatically:
  1. Calculates the fee from the order’s price and size
  2. Signs an EIP-712 authorization (free, off-chain)
  3. Pulls the fee into escrow before the order reaches the CLOB
  4. Refunds the fee automatically if the order is cancelled

Fee Calculation

The fee is calculated as:
feeAmount = floor(price × size × 1e6 × feeBps / 10000)
For example, BUY 10 shares at $0.55 with feeBps: 50:
floor(0.55 × 10 × 1e6 × 50 / 10000) = 27500 raw USDC = $0.0275

Placing an Order with Fees

// Initialize wallet (one-time setup — handles Safe deploy + all 7 approvals including escrow)
const status = await trader.ensureReady(privateKey);
console.log('Wallet ready:', status.funderAddress);
// Send USDC.e to status.funderAddress before placing orders

// Find a market
const results = await fetch('https://api.polynode.dev/v1/search?q=hungary', {
  headers: { 'x-api-key': 'pn_live_...' },
}).then(r => r.json());

const market = results.results[0];
console.log(market.question, '→ token:', market.token_ids[0]);

// Check the orderbook
const book = await fetch(`https://api.polynode.dev/v1/orderbook/${market.token_ids[0]}`, {
  headers: { 'x-api-key': 'pn_live_...' },
}).then(r => r.json());

console.log('Best bid:', book.bids[0]?.price, 'Best ask:', book.asks[0]?.price);

// Place order — fee escrow happens automatically
const result = await trader.order({
  tokenId: market.token_ids[0],
  side: 'BUY',
  price: 0.02,    // well below best ask, will rest on book
  size: 10,
});

console.log('Order ID:', result.orderId);
console.log('Fee TX:', result.feeEscrowTxHash);   // on-chain pullFee transaction
console.log('Fee:', result.feeAmount, 'USDC');     // fee amount charged
The response includes feeEscrowTxHash (the on-chain transaction that moved USDC into escrow) and feeAmount (how much was charged).

Cancelling (Auto-Refund)

if (result.orderId) {
  const cancel = await trader.cancelOrder(result.orderId);
  console.log('Cancelled:', cancel.canceled);
}
// Fee is automatically refunded on-chain — no extra call needed
When you cancel an order that had a fee, the refund happens inline during the cancel. The USDC returns to your Safe wallet in the same request. No polling, no background jobs needed. cancelAll() also triggers refunds for every cancelled order that had a fee.

Per-Order Fee Override

Override the global feeConfig on individual orders:
// This order uses a different fee rate and affiliate
const result = await trader.order({
  tokenId: '...',
  side: 'BUY',
  price: 0.55,
  size: 100,
  feeConfig: {
    feeBps: 100,                           // 1% for this order
    affiliate: '0xSpecialPartner...',
    affiliateShareBps: 5000,               // 50/50 split
  },
});

Opting Out

Set feeBps: 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.
// No fee — identical to pre-escrow behavior
const trader = new PolyNodeTrader({ polynodeKey: 'pn_live_...' });

What the SDK Does Under the Hood

When you call trader.order() with feeBps > 0:
SDK                          Cosigner                     FeeEscrow        CLOB
 │                              │                            │               │
 │  1. Calculate fee            │                            │               │
 │  2. Generate escrowOrderId   │                            │               │
 │  3. Sign EIP-712 FeeAuth     │                            │               │
 │                              │                            │               │
 │── POST /submit (w/ fee) ───>│                            │               │
 │                              │── pullFee(feeAuth) ──────>│               │
 │                              │<── tx confirmed ──────────│               │
 │                              │── forward order ─────────────────────────>│
 │                              │<── orderId ──────────────────────────────│
 │<── { orderId, feeTxHash } ──│                            │               │
 │                              │                            │               │
 │── DELETE /submit (cancel) ─>│                            │               │
 │                              │── forward cancel ────────────────────────>│
 │                              │<── canceled ─────────────────────────────│
 │                              │── refund() ──────────────>│               │
 │                              │<── tx confirmed ──────────│               │
 │<── { canceled } ────────────│                            │               │

Approvals

The escrow contract requires one USDC approval. This is automatically included in the approval batch during ensureReady() — 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).