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

# Trade Tracking Guide

> How to track Polymarket trades for copy trading, analytics, and bookkeeping.

polynode gives you two layers of trade data. Which one you use depends on what you're building.

## Two data layers, one subscription

When you subscribe to `settlements`, you get two events per trade:

1. **`settlement`** (pending) — arrives 3–5 seconds before the block. Decoded from the transaction's calldata while it's still in the mempool.
2. **`status_update`** (confirmed) — arrives when the block confirms. Includes `confirmed_fills` with exact execution data from on-chain `OrderFilled` receipt logs.

Both come through the same subscription. No extra configuration needed.

## How they differ

The pending settlement and confirmed fills come from different data sources inside the same transaction:

|                     | Pending settlement                                                           | Confirmed fills                                       |
| ------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------- |
| **Source**          | Transaction calldata (input)                                                 | OrderFilled receipt logs (output)                     |
| **Speed**           | 3–5 seconds before the block                                                 | At block confirmation                                 |
| **Price accuracy**  | Exact for single-maker fills. Off by 0.01–0.04 on \~5% of multi-maker fills. | Exact. Always. This is the on-chain canonical record. |
| **Size**            | Gross token amount                                                           | Gross token amount (separate `fee` field)             |
| **Per-fill detail** | Per-maker breakdown from calldata                                            | Per-maker breakdown from receipt logs                 |

**Why the difference exists:** When a taker order sweeps multiple makers in one transaction, the calldata contains each maker's order and the total amounts being exchanged. But the total USDC is an aggregate across all makers. polynode estimates each maker's share proportionally. The contract computes and emits the exact per-maker amounts in the `OrderFilled` logs. For most fills, the estimate and the result are identical. For multi-maker sweeps, the contract's integer rounding can produce slightly different per-maker splits.

**Size vs Polymarket:** Polymarket's activity API reports sizes net of fees. polynode's confirmed fills report the gross size from the OrderFilled event plus the fee as a separate field. To get the Polymarket-equivalent size: `net_size = size - (fee / price)`.

## Use case 1: Copy trading

For copy trading, use the **pending settlement**. Speed matters more than the 0.01 price difference on the occasional multi-maker fill.

To find the wallet's actual trade in a settlement event, iterate `data.trades[]` and match by `maker`:

```javascript theme={null}
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type !== "settlement" || msg.data.status !== "pending") return;

  for (const fill of msg.data.trades) {
    if (fill.maker.toLowerCase() === WALLET_IM_COPYING.toLowerCase()) {
      // This is the wallet's actual trade — same side, same token, same price
      placeTrade(fill.token_id, fill.side, fill.price);
    }
  }
};
```

<Warning>
  Always match by `fill.maker`, never by `fill.taker`. The `taker` field on each fill is the counterparty, not the wallet you're tracking. Matching by `taker` returns the wrong wallet's perspective with the **opposite token** and the **complement price** — copy trading from that data would mirror the inverse of every trade.
</Warning>

The pending settlement tells you what the wallet is trading, which direction, and at what price. That's all you need to mirror the trade before the block confirms.

## Use case 2: Analytics and bookkeeping

For trade logs, P\&L tracking, portfolio analytics, or any scenario where you need exact prices, use `confirmed_fills` from the `status_update`.

```javascript theme={null}
const tradeLog = [];

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type !== "status_update" || !msg.data.confirmed_fills) return;

  for (const fill of msg.data.confirmed_fills) {
    if (fill.maker === WALLET_IM_TRACKING) {
      tradeLog.push({
        tx_hash: msg.data.tx_hash,
        block: msg.data.block_number,
        timestamp: msg.data.confirmed_at,
        market: msg.data.market_title,
        token_id: fill.token_id,
        side: fill.side,
        price: fill.price,
        size: fill.size,
        fee: fill.fee,
        order_hash: fill.order_hash,
      });
    }
  }
};
```

This is the same data Polymarket reads from the blockchain. Prices will match Polymarket's activity data exactly.

## Use case 3: Full lifecycle tracking

Track both layers to get the complete picture — early detection plus exact execution. Match by `maker` in both the pending `trades[]` array and the confirmed `confirmed_fills[]` array:

```javascript theme={null}
const pending = new Map();

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  if (msg.type === "settlement" && msg.data.status === "pending") {
    // Find the wallet's actual leg in the pending trades array
    const myLeg = msg.data.trades.find(
      t => t.maker.toLowerCase() === WALLET_IM_TRACKING.toLowerCase()
    );
    if (myLeg) {
      pending.set(msg.data.tx_hash, {
        detected_at: Date.now(),
        pending_price: myLeg.price,
        pending_side: myLeg.side,
      });
    }
  }

  if (msg.type === "status_update" && msg.data.confirmed_fills) {
    const original = pending.get(msg.data.tx_hash);
    const lead_time = original ? msg.data.confirmed_at - original.detected_at : null;

    // Match by maker in confirmed_fills too — same rule
    for (const fill of msg.data.confirmed_fills) {
      if (fill.maker.toLowerCase() === WALLET_IM_TRACKING.toLowerCase()) {
        console.log({
          market: msg.data.market_title,
          side: fill.side,
          pending_price: original?.pending_price,
          confirmed_price: fill.price,
          lead_time_ms: lead_time,
          block: msg.data.block_number,
        });
      }
    }

    pending.delete(msg.data.tx_hash);
  }
};
```

## Confirmed fills field reference

Each object in the `confirmed_fills` array:

| Field          | Type   | Description                                     |
| -------------- | ------ | ----------------------------------------------- |
| `order_hash`   | string | EIP-712 order hash for this fill                |
| `maker`        | string | Maker wallet address                            |
| `taker`        | string | Taker wallet or exchange contract               |
| `token_id`     | string | Conditional token ID for this fill              |
| `side`         | string | `"BUY"` or `"SELL"` (maker's perspective)       |
| `price`        | number | Exact execution price                           |
| `size`         | number | Gross token amount (before fees)                |
| `maker_amount` | string | Raw maker amount (integer, 6 decimals for USDC) |
| `taker_amount` | string | Raw taker amount (integer)                      |
| `fee`          | number | Fee in USDC, or `null`                          |

## Ghost fills and the nonce exploit

A small percentage of Polymarket settlements (\~0.15–0.35%) fail on-chain. These are "ghost fills" — trades that appear to match off-chain but revert during on-chain settlement. The most common cause is the `incrementNonce()` exploit on Polymarket's V1 CTF Exchange, which allows users to invalidate their own orders after matching but before settlement.

This is another reason to use `confirmed_fills` for any application where trade accuracy matters. A pending `settlement` event tells you a trade was matched. Only a `status_update` with `confirmed_fills` tells you it actually settled on-chain.

To detect ghost fills, track pending settlements that never receive a `status_update`:

```javascript theme={null}
const pending = new Map();

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  if (msg.type === "settlement" && msg.data.status === "pending") {
    pending.set(msg.data.tx_hash, {
      ...msg.data,
      detected_at: Date.now(),
    });
  }

  if (msg.type === "status_update") {
    // Trade confirmed on-chain — not a ghost fill
    pending.delete(msg.data.tx_hash);
  }
};

// Pending settlements older than 15 seconds without a status_update are ghost fills
setInterval(() => {
  const now = Date.now();
  for (const [txHash, data] of pending) {
    if (now - data.detected_at > 15000) {
      console.log("Ghost fill detected:", txHash);
      pending.delete(txHash);
    }
  }
}, 5000);
```

For detailed research on the exploit, affected markets, and attacker identification, see the [Nonce Exploit Research](/guides/nonce-exploit) page.
