> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polynode.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Fee Escrow

> Optional per-order fee collection with on-chain escrow, refund on cancel, and affiliate revenue sharing.

## 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.

<Note>
  **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:

  | 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  |

  All three are independent. `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.
</Note>

## Frequently Asked Questions

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="What stops someone else from taking my fee out of the contract?">
    Three access controls, each with a single hardcoded destination:

    * **`distribute()`** requires `onlyAuthorized` (our operator wallet or contract owner). Sends funds only to the `affiliate` and `treasury` addresses stored at deposit time. Anyone else calling it gets `Unauthorized`.
    * **`refund()`** also requires `onlyAuthorized`. Sends funds only back to the original `payer`.
    * **`claimRefund()`** can be called by anyone, but the contract checks `msg.sender == escrow.payer` and `block.timestamp > pulledAt + 72 hours`. Only the original payer can claim, and only after 72h. Funds go exclusively to `escrow.payer`.

    The affiliate address and share split are signed by the user in the EIP-712 FeeAuth message and stored on-chain at deposit time. Nobody can change them after deposit. The user signed it, the contract stored it, and that's where the money goes.
  </Accordion>

  <Accordion title="Where exactly does the money go when distributed?">
    The contract splits every distribution into two transfers:

    * `affiliateShareBps / 10000` of the amount goes to the `affiliate` address (your wallet).
    * The remainder goes to the `treasury` address (set by the contract owner).

    If `affiliateShareBps` is `10000` (the default), you keep 100% and nothing goes to the treasury. Both addresses are immutable for each escrow entry.
  </Accordion>

  <Accordion title="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:

    1. **Inline refund on cancel.** When you call `cancelOrder()` or `cancelAll()`, our cosigner automatically calls `refund()` in the same request. The collateral returns to the user's wallet immediately.
    2. **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.
    3. **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.

    These three paths are completely independent. If our backend is down, path 3 still works. If the user is offline, paths 1 and 2 still work.
  </Accordion>

  <Accordion title="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.

    There is no expiration that burns funds. There is no third destination. If all automated paths fail, the contract owner has `emergencyWithdraw` as a last resort. And `claimRefund` is always available to the payer after 72 hours with zero dependencies on our infrastructure.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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 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.
  </Accordion>

  <Accordion title="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 in every matching 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.
  </Accordion>
</AccordionGroup>

## 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             |

The two contracts have byte-identical calldata, identical function signatures, and identical behavior. The only differences are the collateral token (USDC.e vs pUSD) and the EIP-712 domain name / version. If you're writing a new V2 integration you do not need to support V1 — just point at the V2 contract and sign against the V2 domain.

Existing V1 integrations keep working unchanged — no migration required.

## How It Works

```
1. User signs EIP-712 fee authorization (off-chain, free)
2. Operator calls pullFee → collateral 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      → collateral returns to user's Safe
   On timeout (72h) → user calls claimRefund → collateral 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 ERC-20 transfer (USDC.e on V1, pUSD on V2) that happens before the order.

## Contract addresses

Both contracts are on Polygon mainnet (chain ID 137), both use the same 72-hour self-refund window, and both have the same `FEE_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).

```typescript theme={null}
// V1: ensureReady() approves USDC.e for the V1 FeeEscrow alongside
//     the six Polymarket approvals. No manual setup needed.
const status = await trader.ensureReady(privateKey);
```

For V2, set `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: `polynode-sdk@0.9.2` (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. The `FeeAuth` struct is identical across V1 and V2 — only the domain changes.

```typescript theme={null}
// V1 domain (USDC.e collateral)
{
  name: "PolyNodeFeeEscrow",
  version: "1",
  chainId: 137,
  verifyingContract: "0xa11D28433B79D0A88F3119b16A090075752258EA"
}

// V2 domain (pUSD collateral)
{
  name: "PolyNodeFeeEscrowV2",
  version: "2",
  chainId: 137,
  verifyingContract: "0x3A43D88ef8Aae4dF5a50B3abf67122CAAeEF7c9F"
}

// FeeAuth struct — same for V1 and V2
{
  orderId: bytes32,   // unique order identifier
  payer: address,     // user's Safe wallet (where collateral is pulled from)
  signer: address,    // user's EOA (signs this message)
  feeAmount: uint256, // fee amount (6 decimals — USDC.e on V1, pUSD on V2)
  deadline: uint256,  // unix timestamp — authorization expires after this
  nonce: uint256      // signer's current nonce (prevents replay)
}
```

### Selecting V2 at submit time

The polynode cosigner routes every fee request to V1 by default. To use V2, include the contract address in the `fee_auth` body of your `/submit` call:

```jsonc theme={null}
// POST /submit to trade.polynode.dev
{
  "method": "POST",
  "path": "/order",
  "body": "...",                     // V2 CLOB order body
  "headers": { ... },                // CLOB L2 HMAC headers
  "fee_auth": {
    "escrow_contract": "0x3A43D88ef8Aae4dF5a50B3abf67122CAAeEF7c9F",  // ← routes to V2
    "escrow_order_id": "0x...",
    "payer":  "0xSafe...",
    "signer": "0xEOA...",
    "fee_amount": "27500",
    "deadline": 1777000000,
    "nonce": 0,
    "signature": "0x...",            // signed against the V2 domain above
    "affiliate": "0xPartnerWallet...",
    "affiliate_share_bps": 10000
  }
}
```

Omit `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](#72-hour-safety-net) section below.

```
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.

```typescript theme={null}
// 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
```

| 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    |

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

The escrow contract has a 72-hour timeout. After that window, the user can always get their undistributed fee back.

```solidity theme={null}
// After 72 hours, anyone can call this
escrow.claimRefund(orderId);
// → undistributed USDC returns to the payer's Safe, always
```

Three things to know:

1. **The transfer target is hardcoded.** `claimRefund` always 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.
2. **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.
3. **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 and `distribute` / `refund` continue 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. |

In practice, virtually no user will invoke `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 `refund` before 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.

Funds are never permanently locked, never routed to a third party, and never surprised away from either the user or the builder during the normal fill path.

## 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

```typescript theme={null}
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:

```typescript theme={null}
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

```typescript theme={null}
// 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)

```typescript theme={null}
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:

```typescript theme={null}
// 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.

```typescript theme={null}
// 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).
