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:
settlement (pending) — arrives 3–5 seconds before the block. Decoded from the transaction’s calldata while it’s still in the mempool.
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 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:
| 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:
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.