Skip to main content
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 settlementConfirmed fills
SourceTransaction calldata (input)OrderFilled receipt logs (output)
Speed3–5 seconds before the blockAt block confirmation
Price accuracyExact 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.
SizeGross token amountGross token amount (separate fee field)
Per-fill detailPer-maker breakdown from calldataPer-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 does its own math internally 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:
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);
    }
  }
};
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.
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.
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:
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:
FieldTypeDescription
order_hashstringEIP-712 order hash for this fill
makerstringMaker wallet address
takerstringTaker wallet or exchange contract
token_idstringConditional token ID for this fill
sidestring"BUY" or "SELL" (maker’s perspective)
pricenumberExact execution price
sizenumberGross token amount (before fees)
maker_amountstringRaw maker amount (integer, 6 decimals for USDC)
taker_amountstringRaw taker amount (integer)
feenumberFee 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:
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 page.