Decoding the Digital Tea Leaves: A Guide to Analyzing Polymarket’s On-Chain Order Data

18 minute read | Published:

What if you could tap directly into the collective wisdom of thousands of speculators on major world events? Welcome to the world of Polymarket, a leading decentralized prediction market on the Polygon network. It’s more than just a betting platform; it’s a real-time, transparent feed of user sentiment, with every prediction, trade, and transaction permanently etched onto the blockchain.

By learning to read this on-chain data, you can unlock unparalleled insights into market dynamics, user behavior, and the intricate mechanics of decentralized finance. Whether you’re a data analyst, a crypto enthusiast, or a seasoned trader, this guide will equip you with the skills to navigate Polymarket’s on-chain world.

This article will break down how to access and interpret Polymarket’s on-chain order data, transforming you from a casual observer into a savvy on-chain analyst.

I. The On-Chain Foundation: Essential Background Knowledge

Before we dive into the data, it’s crucial to understand the smart contracts that power Polymarket. Think of these as the digital bedrock upon which the entire platform is built.

Key Smart Contracts

To navigate Polymarket’s on-chain presence, familiarize yourself with these primary smart contract addresses on the Polygon network and their roles:

Contract Name/RolePolygon AddressNotes
Conditional Tokens Framework (CTF)0x4D97DCd97eC945f40cF65F87097ACe5EA0476045The core Gnosis contract for creating and managing ERC-1155 outcome shares. This is the engine that generates the very tokens you’ll be analyzing.
CTF Exchange0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982EThe main exchange for settling trades in standard binary (YES/NO) markets through atomic swaps of outcome tokens and USDC.
NegRisk_CTFExchange0xC5d563A36AE78145C45a50134d48A1215220f80aA specialized exchange for the more complex multi-outcome markets that utilize the NegRiskAdapter.
NegRiskAdapter0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296A key innovation that adapts the Gnosis CTF for multi-outcome markets by structuring them from underlying binary components, cleverly managing “NO” to “YES” token conversions.
USDC.e (Collateral)0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174The bridged USDC stablecoin used as the universal collateral for betting and payouts on the platform.
UMA Oracle (V2.0.0)0x6A9D222616C90FcA5754cd1333cFD9b7fb6a4F74The decentralized oracle responsible for the crucial task of resolving market outcomes.
Gnosis Safe Proxy Factory0xaacfeea03eb1561c4e67d661e40682bd20e3541bCreates proxy wallets for users interacting with Polymarket via MetaMask.
Polymarket Proxy Factory0xaB45c5A4B0c941a2F231C04C3f49182e1A254052Creates proxy wallets for users who sign up and interact using MagicLink.
Polymarket: Neg Risk Fee Module0x78769D50Be1763ed1CA0D5E878D93f05aabff29eThis contract works with the NegRisk_CTFExchange to apply the correct fee logic for trades in multi-outcome markets.

All trading activity on Polymarket is settled through two main contracts: the CTF Exchange for simple YES/NO markets, and the NegRisk_CTFExchange for more complex, multi-outcome markets. It’s the events emitted by these two contracts that we’ll be focusing on.

The Lifecycle of an Outcome Token: Minting and Burning

Before we analyze trades, we need to understand what is being traded. Polymarket uses the Gnosis Conditional Tokens Framework to represent market outcomes.

For any given prediction market, like “Will Candidate A win the election?”, Polymarket creates a pair of outcome tokens: a “YES” token and a “NO” token. Each pair is backed by 1 USDC of collateral. These outcome tokens have a unique on-chain ID, the positionId, ensuring each token is tied to a specific outcome of a specific event.

Token Minting (Creation):

Imagine two bettors with opposing views:

  • Bettor X is willing to pay 0.70 USDC for a “YES” token.
  • Bettor Y is willing to pay 0.30 USDC for a “NO” token.

Polymarket’s matching engine brings them together. Their combined 1 USDC (0.70 + 0.30) is locked as collateral, and a new pair of “YES” and “NO” tokens is minted. Bettor X receives the “YES” token, and Bettor Y receives the “NO” token.

If Candidate A wins, the holder of the “YES” token can redeem it for the full 1 USDC of collateral. If Candidate A loses, the “NO” token holder gets the 1 USDC.

Token Burning (Redemption):

Traders don’t have to wait for the market to resolve. They can sell their outcome tokens at any time. A sell order can be matched with a buy order from another trader. However, a sell order for a “YES” token can also be matched with a sell order for the corresponding “NO” token.

For example:

  • Trader A wants to sell their “YES” token for 0.60 USDC.
  • Trader B wants to sell their “NO” token for 0.40 USDC.

The exchange can match these two sell orders. The pair of outcome tokens is burned, and the 1 USDC of collateral is released: 0.60 USDC goes to Trader A, and 0.40 USDC goes to Trader B.

II. Accessing the Data: Your On-Chain Toolkit

There are two primary ways to access Polymarket’s on-chain data:

  1. Directly from the Blockchain: By querying a Polygon RPC endpoint. This method gives you raw, unfiltered access to the data.
  2. Using The Graph: A decentralized indexing protocol that makes querying blockchain data much simpler and more efficient. For beginners, this is the recommended starting point.

In this article, we will focus on the direct RPC method to understand the fundamentals, be aware that tools like The Graph can significantly speed up your analysis.

To extract Polymarket order data for a specific prediction market, we need to examine transactions involving either the CTF Exchange or NegRisk_CTFExchange addresses when they execute the matchOrders function. You can find these activities in the Token Transfers (ERC-20) section on PolygonScan.

There are two primary methods to access settled Polymarket order data:

  1. Transaction Input Data: Analyzing the input data section of transactions where either exchange contract executes the matchOrders function.
  2. Event Logs: Examining the OrderFilled and OrdersMatched event logs emitted by these exchange contracts.

I recommend the second method for most use cases. While the first method shows the original order amounts, it doesn’t reflect the actual settled amounts in each specific transaction. This is because a single order maker can serve as a liquidity provider across multiple separate transactions, making it harder to track individual trade executions.

The analysis procedures are nearly identical for both CTF Exchange and NegRisk_CTFExchange. For consistency, I’ll use NegRisk_CTFExchange in the following examples. If you’re analyzing data from binary outcome prediction markets, simply substitute the NegRisk_CTFExchange address with the CTF Exchange address and use the corresponding ABI file for data decoding.

Tapping into the Blockchain with Python

First, you’ll need to connect to a Polygon RPC (Remote Procedure Call) endpoint. Public RPCs, like https://polygon-rpc.com, are a good starting point but may have rate limits. For more intensive analysis, consider private RPCs from providers like Ankr, Alchemy, and Infura.

Here’s how you can fetch transaction logs using Python and the web3.py library:

from web3 import Web3
from web3.middleware import ExtraDataToPOAMiddleware

# Establish connection to a Polygon RPC endpoint
POLYGON_RPC_URL = "https://polygon-rpc.com/"
web3 = Web3(Web3.HTTPProvider(POLYGON_RPC_URL))

# Polygon uses a Proof-of-Authority consensus mechanism, so we need to inject this middleware
web3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)

# Define the block number and the target contract address we want to inspect
block_number = 51866068
# For this example, we'll look at the exchange for multi-outcome markets
TARGET_ADDRESS = "0xc5d563a36ae78145c45a50134d48A1215220f80a" # NegRisk_CTFExchange address

# Fetch all logs for the target address within the specified block range
logs = web3.eth.get_logs({
    'fromBlock': block_number,
    'toBlock': block_number,
    'address': Web3.to_checksum_address(TARGET_ADDRESS)
})

print(logs)

Running this script will return a list of logs, which at first glance might seem like gibberish:

 [AttributeDict({'address': '0xC5d563A36AE78145C45a50134d48A1215220f80a',
  'topics': [HexBytes('0xd0a08e8c493f9c94f29311604c9de1b4e8c8d4c06bd0c789af57f2d65bfec0f6'),
   HexBytes('0x83b04dd4f7591c60e21694ce5808587fa5a331bb958994389ce95eddfdb148c6'),
   HexBytes('0x0000000000000000000000003cf3e8d5427aed066a7a5926980600f6c3cf87b3'),
   HexBytes('0x000000000000000000000000d42f6a1634a3707e27cbae14ca966068e5d1047d')],
  'data': HexBytes('0x6f3dc129ae1b3a62bd59c83235f421bb772d0bca3038a6f36df840e6577aab88000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009896800000000000000000000000000000000000000000000000000000000000325aa00000000000000000000000000000000000000000000000000000000000000000'),
  'blockNumber': 51866068,
  'transactionHash': HexBytes('0x73ca58d58325b5cbb369588671844dd555dcb33723497e96097b196b8bdfdcd8'),
  'transactionIndex': 7,
  'blockHash': HexBytes('0xa2299601b0134f9e8c4107a401b5b9f899446f31cac59bb8ce1490f2271c59cc'),
  'logIndex': 25,
  'removed': False}),

 AttributeDict({'address': '0xC5d563A36AE78145C45a50134d48A1215220f80a',
  'topics': [HexBytes('0xd0a08e8c493f9c94f29311604c9de1b4e8c8d4c06bd0c789af57f2d65bfec0f6'),
   HexBytes('0x55a5da3494e8670f67e8952b61ea620bf0939a84065671ba2f4e2930653a7d3c'),
   HexBytes('0x000000000000000000000000d42f6a1634a3707e27cbae14ca966068e5d1047d'),
   HexBytes('0x000000000000000000000000c5d563a36ae78145c45a50134d48a1215220f80a')],
  'data': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000006f3dc129ae1b3a62bd59c83235f421bb772d0bca3038a6f36df840e6577aab880000000000000000000000000000000000000000000000000000000000325aa000000000000000000000000000000000000000000000000000000000009896800000000000000000000000000000000000000000000000000000000000000000'),
  'blockNumber': 51866068,
  'transactionHash': HexBytes('0x73ca58d58325b5cbb369588671844dd555dcb33723497e96097b196b8bdfdcd8'),
  'transactionIndex': 7,
  'blockHash': HexBytes('0xa2299601b0134f9e8c4107a401b5b9f899446f31cac59bb8ce1490f2271c59cc'),
  'logIndex': 27,
  'removed': False}),

 AttributeDict({'address': '0xC5d563A36AE78145C45a50134d48A1215220f80a',
  'topics': [HexBytes('0x63bf4d16b7fa898ef4c4b2b6d90fd201e9c56313b65638af6088d149d2ce956c'),
   HexBytes('0x55a5da3494e8670f67e8952b61ea620bf0939a84065671ba2f4e2930653a7d3c'),
   HexBytes('0x000000000000000000000000d42f6a1634a3707e27cbae14ca966068e5d1047d')],
  'data': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000006f3dc129ae1b3a62bd59c83235f421bb772d0bca3038a6f36df840e6577aab880000000000000000000000000000000000000000000000000000000000325aa00000000000000000000000000000000000000000000000000000000000989680'),
  'blockNumber': 51866068,
  'transactionHash': HexBytes('0x73ca58d58325b5cbb369588671844dd555dcb33723497e96097b196b8bdfdcd8'),
  'transactionIndex': 7,
  'blockHash': HexBytes('0xa2299601b0134f9e8c4107a401b5b9f899446f31cac59bb8ce1490f2271c59cc'),
  'logIndex': 28,
  'removed': False})]

These logs contain the topics and data fields, which hold the key to understanding each transaction. To decipher them, we need the contract’s ABI.

Decoding the Data with an ABI

An ABI (Application Binary Interface) is a JSON file that acts as a blueprint for a smart contract. It tells us how to interpret the contract’s functions and events. You can find the ABI for any verified contract on PolygonScan. For the NegRisk_CTFExchange, you can find it on its contract page under the “Contract” tab.

Once you have the ABI file (e.g., Polymarket_NegRisk_CTFExchange_abi.json), you can use the following Python code to decode the logs into a human-readable format:

(1)First, we create an event signature mapping, so we can translate items like HexBytes('0xd0a08e8c493f9c94f29311604c9de1b4e8c8d4c06bd0c789af57f2d65bfec0f6') into the event name that we can read.

from web3 import Web3
w3 = Web3()
# Load the ABI
with open('Polymarket_NegRisk_CTFExchange_abi.json', 'r') as abi_file:
    contract_abi = json.load(abi_file)

contract_address = '0xC5d563A36AE78145C45a50134d48A1215220f80a' # NegRisk_CTFExchange address
contract_address = Web3.to_checksum_address(contract_address) 
contract = w3.eth.contract(address=contract_address, abi=contract_abi)

# Create Event Signature Mapping
event_signature_to_event = {}
for abi_item in contract_abi:
    if abi_item['type'] == 'event':
        event_obj = contract.events.__getattr__(abi_item['name'])()
        # Create event signature string and hash it
        event_signature = f"{abi_item['name']}({','.join([param['type'] for param in abi_item['inputs']])})"
        signature_hash = Web3.keccak(text=event_signature)
        signature_hash = '0x' + signature_hash.hex() # convert to hex
        event_signature_to_event[signature_hash] = event_obj

We get a mapping between the event IDs and the events inside this contract:

{'0xacffcc86834d0f1a64b0d5a675798deed6ff0bcfc2231edd3480e7288dba7ff4': <Event FeeCharged(address,uint256,uint256)>,
 '0xf9ffabca9c8276e99321725bcb43fb076a6c66a54b7f21c4e8146d8519b417dc': <Event NewAdmin(address,address)>,
 '0xf1e04d73c4304b5ff164f9d10c7473e2a1593b740674a6107975e2a7001c1e5c': <Event NewOperator(address,address)>,
 '0x5152abf959f6564662358c2e52b702259b78bac5ee7842a0f01937e670efcc7d': <Event OrderCancelled(bytes32)>,
 '0xd0a08e8c493f9c94f29311604c9de1b4e8c8d4c06bd0c789af57f2d65bfec0f6': <Event OrderFilled(bytes32,address,address,uint256,uint256,uint256,uint256,uint256)>,
 '0x63bf4d16b7fa898ef4c4b2b6d90fd201e9c56313b65638af6088d149d2ce956c': <Event OrdersMatched(bytes32,address,uint256,uint256,uint256,uint256)>,
 '0x3053c6252a932554235c173caffc1913604dba3a41cee89516f631c4a1a50a37': <Event ProxyFactoryUpdated(address,address)>,
 '0x787a2e12f4a55b658b8f573c32432ee11a5e8b51677d1e1e937aaf6a0bb5776e': <Event RemovedAdmin(address,address)>,
 '0xf7262ed0443cc211121ceb1a80d69004f319245615a7488f951f1437fd91642c': <Event RemovedOperator(address,address)>,
 '0x9726d7faf7429d6b059560dc858ed769377ccdf8b7541eabe12b22548719831f': <Event SafeFactoryUpdated(address,address)>,
 '0xbc9a2432e8aeb48327246cddd6e872ef452812b4243c04e6bfb786a2cd8faf0d': <Event TokenRegistered(uint256,uint256,bytes32)>,
 '0x203c4bd3e526634f661575359ff30de3b0edaba6c2cb1eac60f730b6d2d9d536': <Event TradingPaused(address)>,
 '0xa1e8a54850dbd7f520bcc09f47bff152294b77b2081da545a7adf531b7ea283b': <Event TradingUnpaused(address)>}

Now we know in the above two event logs: 0xd0a08e8c493f9c94f29311604c9de1b4e8c8d4c06bd0c789af57f2d65bfec0f6 is a OrderFilled event, and 0x63bf4d16b7fa898ef4c4b2b6d90fd201e9c56313b65638af6088d149d2ce956c is a OrdersMatched event.

(2)Second, we can now decode the whole topic and data sections:

# Define the Decoding Function
def decode_log(raw_log, event_signature_to_event):
    event_signature = '0x' + raw_log['topics'][0].hex()
    event = event_signature_to_event.get(event_signature)

    log_dict = {
        'address': raw_log['address'], 
        'topics': raw_log['topics'],
        'data': raw_log['data'],
        'blockNumber': raw_log['blockNumber'],
        'blockHash': raw_log['blockHash'],
        'transactionHash': raw_log['transactionHash'],
        'transactionIndex': raw_log['transactionIndex'],
        'logIndex': raw_log['logIndex'],
    }
    
    decoded = event.process_log(log_dict)
    decoded_args = decoded['args']
    decoded_args['event'] = decoded['event']
    return decoded_args

# Apply the Decoding Function
# (1) raw_log 1 and 2 are OrderFilled event
raw_log_1 = logs[0]
decoded_log_1 = decode_log(raw_log_1, event_signature_to_event)
# convert orderHash from bytes to hex
decoded_log_1['orderHash'] = '0x' + decoded_log_1['orderHash'].hex()

raw_log_2 = logs[1]
decoded_log_2 = decode_log(raw_log_2, event_signature_to_event)
decoded_log_2['orderHash'] = '0x' + decoded_log_2['orderHash'].hex()

# (2) raw_log 3 is OrderMatched event
raw_log_3 = logs[2]
decoded_log_3 = decode_log(raw_log_3, event_signature_to_event)
# convert takerOrderHash from bytes to hex
decoded_log_3['takerOrderHash'] = '0x' + decoded_log_3['takerOrderHash'].hex()

Now the topic and data sections in the above three logs can be fully decoded into:

{'orderHash': '0x83b04dd4f7591c60e21694ce5808587fa5a331bb958994389ce95eddfdb148c6',
 'maker': '0x3Cf3E8d5427aED066a7A5926980600f6C3Cf87B3',
 'taker': '0xd42F6a1634A3707e27cBae14ca966068E5D1047d',
 'makerAssetId': 50315837024432334213827041057729556211989649223066002327303150792784314280840,
 'takerAssetId': 0,
 'makerAmountFilled': 10000000,
 'takerAmountFilled': 3300000,
 'fee': 0,
 'event': 'OrderFilled'}

{'orderHash': '0x55a5da3494e8670f67e8952b61ea620bf0939a84065671ba2f4e2930653a7d3c',
 'maker': '0xd42F6a1634A3707e27cBae14ca966068E5D1047d',
 'taker': '0xC5d563A36AE78145C45a50134d48A1215220f80a',
 'makerAssetId': 0,
 'takerAssetId': 50315837024432334213827041057729556211989649223066002327303150792784314280840,
 'makerAmountFilled': 3300000,
 'takerAmountFilled': 10000000,
 'fee': 0,
 'event': 'OrderFilled'}

{'takerOrderHash': '0x55a5da3494e8670f67e8952b61ea620bf0939a84065671ba2f4e2930653a7d3c',
 'takerOrderMaker': '0xd42F6a1634A3707e27cBae14ca966068E5D1047d',
 'makerAssetId': 0,
 'takerAssetId': 50315837024432334213827041057729556211989649223066002327303150792784314280840,
 'makerAmountFilled': 3300000,
 'takerAmountFilled': 10000000,
 'event': 'OrdersMatched'}

We now have a much clearer, structured output of the event’s data.

III. Interpreting the Data: OrderFilled and OrdersMatched Events

When you decode the logs, you’ll primarily be dealing with two types of events: OrderFilled and OrdersMatched.

The OrderFilled Event

This event is emitted for each individual order that is filled or partially filled. Let’s break down its key fields:

  • orderHash: A unique identifier for the order.
  • maker: The address of the user who placed the limit order (the liquidity provider).
  • taker: The address that filled the order. This can be another user or the exchange contract itself if multiple orders are matched.
  • makerAssetId: The ID of the asset the maker is providing. If it’s 0, the maker is offering USDC (a Buy order for an outcome token). If it’s a long number (the positionId), the maker is offering an outcome token (a Sell order).
  • takerAssetId: The ID of the asset the taker is providing. The logic is the inverse of makerAssetId.
  • makerAmountFilled: The amount of the asset the maker has given out.
  • takerAmountFilled: The amount of the asset the taker has given out.

Example Interpretation:

{   
    'orderHash': '0x83b04dd4f7591c60e21694ce5808587fa5a331bb958994389ce95eddfdb148c6',
    'maker': '0x3Cf3E8d5427aED066a7A5926980600f6C3Cf87B3',
    'taker': '0xd42F6a1634A3707e27cBae14ca966068E5D1047d',
    'makerAssetId': 50315837024432334213827041057729556211989649223066002327303150792784314280840,
    'takerAssetId': 0,
    'makerAmountFilled': 10000000,   // 10 outcome tokens (10 * 10^6)
    'takerAmountFilled': 3300000,   // 3.3 USDC (3.3 * 10^6)
    'fee': 0,
    'event': 'OrderFilled'
}
  • The Maker (0x3Cf3...) initiated a SELL order, providing outcome tokens (makerAssetId is not zero).
  • The Taker (0xd42F...) filled this order, providing USDC (takerAssetId is zero).
  • The Trade: The maker sold 10 outcome tokens and received 3.3 USDC in return.

The OrdersMatched Event

{
    'takerOrderHash': '0x55a5da3494e8670f67e8952b61ea620bf0939a84065671ba2f4e2930653a7d3c',
    'takerOrderMaker': '0xd42F6a1634A3707e27cBae14ca966068E5D1047d',
    'makerAssetId': 0,
    'takerAssetId': 50315837024432334213827041057729556211989649223066002327303150792784314280840,
    'makerAmountFilled': 3300000,
    'takerAmountFilled': 10000000,
    'event': 'OrdersMatched'
}

This event provides a summary when two or more orders are matched. It links the buy and sell orders together. This log shows that in the above matched order, the order taker is 0xd42F.... The taker provids USDC (makerAssetId: 0) and buys token 5031... in this transaction.

When an OrdersMatched event occurs, you will always see at least two corresponding OrderFilled events—one for the buyer and one for the seller.

IV. Putting It All Together: Real-World Scenarios

Let’s analyze a few examples to solidify your understanding.

(1) A Simple Trade: Bettor vs. Bettor

In a typical trade, a buyer and a seller are matched. You will see two OrderFilled events and one OrdersMatched event. One OrderFilled event will show a user selling an outcome token for USDC, and the other will show a user buying that same token with USDC.

The belowing OrderFilled and OrdersMatched logs record a slightly more complex case: a outcome token transactions between three bettors: bettors 0x8698... and 0x5e9f... sell outcome token 5031... and bettor 0x6fd3... buys the otucome token.

log_indextransaction_indexblock_numberorderHashmakertakermakerAssetIdtakerAssetIdmakerAmountFilledtakerAmountFilledeventtakerOrderHashtakerOrderMakerblock_timestamp
15234518653400x46b375b7a0f526ef3e6c122f38aaa5a3390430546454…0x8698EdBeFd013dB6D087E3d09EEFa08e40bC35c10x32e3742A6DD363c3DFDba700b77f845Ec99aD0665031583702443233421382704105772955621198964922…0111820000.034664200.0OrderFilledNoneNone2024-01-02 20:27:28 UTC
15534518653400x5e9f8e263b2b8bf538f91595472a0cb98d2bd3333b8d…0x3Cf3E8d5427aED066a7A5926980600f6C3Cf87B30x32e3742A6DD363c3DFDba700b77f845Ec99aD0665031583702443233421382704105772955621198964922…010000000.03100000.0OrderFilledNoneNone2024-01-02 20:27:28 UTC
15734518653400x6fd37744eff1c1c11e413b2395445c06c0bd04dd6f85…0x32e3742A6DD363c3DFDba700b77f845Ec99aD0660xC5d563A36AE78145C45a50134d48A1215220f80a05031583702443233421382704105772955621198964922…37764200.0121820000.0OrderFilledNoneNone2024-01-02 20:27:28 UTC
1583451865340NoneNoneNone05031583702443233421382704105772955621198964922…37764200.0121820000.0OrdersMatched0x6fd37744eff1c1c11e413b2395445c06c0bd04dd6f85…0x32e3742A6DD363c3DFDba700b77f845Ec99aD0662024-01-02 20:27:28 UTC

Usually, we only need to focus on the maker side:

(1) If the makerAssetId is 0, then it means this bettor is buying a conditional token.

(2) If the makerAssetId is a string like 5031... (the positionID in gnosis conditional token protocol), then it means this bettor is selling this conditional token.

(3) When polymarket is matching more than 2 orders, the taker ID can be the Polymarket NegRisk_CTFExchange address (0xC5d563A36AE78145C45a50134d48A1215220f80a), whoes contract faciliated the match, not the real trading party.

(2) Minting New Tokens

If two users want to bet on opposite outcomes of a binary market, the exchange can mint new tokens for them. In this case, both OrderFilled events will show makerAssetId as 0, as both parties are providing USDC. The takerAssetId in each event will correspond to the “YES” and “NO” outcome tokens, respectively.

log_indextransaction_indexblock_numbermakertakermakerAssetIdtakerAssetIdmakerAmountFilledtakerAmountFilledeventtakerOrderHashtakerOrderMakerblock_timestamp
13744544320340x351A72160E477863D13666c62ef1e7631B3940bB0xE0DbDB7F005f233f4510e4Ef7e53A2f76a8Df44E03473165777088344114087500151809875113887709547…3.960000e+096.000000e+09OrderFilledNoneNone2024-03-09 00:24:25+00:00
13944544320340xE0DbDB7F005f233f4510e4Ef7e53A2f76a8Df44E0xC5d563A36AE78145C45a50134d48A1215220f80a08802783960924362419341561417932867960261291649…2.040000e+096.000000e+09OrderFilledNoneNone2024-03-09 00:24:25+00:00
1404454432034NoneNone08802783960924362419341561417932867960261291649…2.040000e+096.000000e+09OrdersMatched0x02a26ec7f7fade1ca26db91b6b5d5f277f28f0fd5a40…0xE0DbDB7F005f233f4510e4Ef7e53A2f76a8Df44E2024-03-09 00:24:25+00:00

In this case, bettor 0x351A7... wants to buy 6000 BidenLose tokens (3473...) and bettor 0xE0Db... wants to buy 6000 BidenWin tokens (8802...). Both are buy orders, but they want to bet on the opposite results, so Polymarket NegRisk_CTFExchange matched them together, took their USDC (3960 + 2040 = 6000 USDC) and minted new opposite tokens for these two users.

(3) Burning Tokens

If two users want to sell their opposing outcome tokens, the exchange can match them, burn the tokens, and release the collateral. Here, both OrderFilled events will show a non-zero makerAssetId (as they are both providing outcome tokens) and a takerAssetId of 0 (as they are both receiving USDC).

log_indextransaction_indexblock_numberorderHashmakertakermakerAssetIdtakerAssetIdmakerAmountFilledtakerAmountFilledeventtakerOrderHashtakerOrderMakerblock_timestamp
29374519585520xb06de8c9f6c2035e5464f1328a69e56e6c2eb28958a4…0x64C1FFb0283a322bdBE54298713c8013E5E2160F0xff66A0aDa4122C5d9292Ffb7eC02922d167a7A074833104333661288389093875950949315923475504897…0206190000.0123714000.0OrderFilledNoneNone2024-01-05 06:54:00+00:00
29574519585520x53085d51ac4ea1ddce4c3450bcdb5ff7402417f84883…0xff66A0aDa4122C5d9292Ffb7eC02922d167a7A070xC5d563A36AE78145C45a50134d48A1215220f80a2174263314346390629056905015582624153306727273…0206190000.082476000.0OrderFilledNoneNone2024-01-05 06:54:00+00:00
2967451958552NoneNoneNone2174263314346390629056905015582624153306727273…0206190000.082476000.0OrdersMatched0x53085d51ac4ea1ddce4c3450bcdb5ff7402417f84883…0xff66A0aDa4122C5d9292Ffb7eC02922d167a7A072024-01-05 06:54:00+00:00

In this case, bettor 0x64C1... wants to sell 206.19 TrumpLose tokens (4833...) and bettor 0xff66... wants to sell 206.19 TrumpWin tokens (2174...). Their tokens represent the opposite results, so Polymarket NegRisk_CTFExchange matched them together, burned their token and sent the unlocked USDC (123.714 + 82.476 = 206.19 USDC) to these two users. (Remember: one locked USDC can mint out one pair of opposite tokens).

(4) A Mixed Scenario

This is where it gets interesting. Here is a scenario with three traders:

log_indextransaction_indexblock_numberorderHashmakertakermakerAssetIdtakerAssetIdmakerAmountFilledtakerAmountFilledeventtakerOrderHashtakerOrderMakerblock_timestamp
30070519516540x0420e9f7b4867f52c63c2c7d5bbf8240a19ee77629fb…0x8698EdBeFd013dB6D087E3d09EEFa08e40bC35c10xf0b049379BBD6399aD1C6704345a7CeC813968ec2174263314346390629056905015582624153306727273…0200000000.084000000.0OrderFilledNoneNone2024-01-05 02:36:24 UTC
31470519516540x87b9de8b3dc9bdcbf7e05cb4dd4e6c6f4d7a461a8866…0xd42F6a1634A3707e27cBae14ca966068E5D1047d0xf0b049379BBD6399aD1C6704345a7CeC813968ec04833104333661288389093875950949315923475504897…22095238.038095237.0OrderFilledNoneNone2024-01-05 02:36:24 UTC
31670519516540x4cd69226a72cda1b9acc16b7cc4d350b8076f82635d1…0xf0b049379BBD6399aD1C6704345a7CeC813968ec0xC5d563A36AE78145C45a50134d48A1215220f80a02174263314346390629056905015582624153306727273…99999999.0238095237.0OrderFilledNoneNone2024-01-05 02:36:24 UTC
3177051951654NoneNoneNone02174263314346390629056905015582624153306727273…99999999.0238095237.0OrdersMatched0x4cd69226a72cda1b9acc16b7cc4d350b8076f82635d1…0xf0b049379BBD6399aD1C6704345a7CeC813968ec2024-01-05 02:36:24 UTC

In this case, bettor 0x8698... sells 200 TrumpWin tokens (2174...) for 84 USDC, bettor 0xd42F... buys 38.095237 TrumLose tokens (4833...) for 22.095238 USDC, bettor 0xf0b0... buys 238.095237 TrumpWin tokens (2174...) for 99.999999 USDC.

The NegRisk_CTFExchange can facilitate this complex transaction:

  1. Direct Trade: 200 of bettor 0xf0b0...’s TrumpWin buy order are matched with bettor 0x8698...’s sell order.
  2. Minting: The remaining 38.095237 TrumpWin from 0xf0b0...’s order are matched with bettor 0xd42F...’s “TrumpLose” order. The combined USDC is used to mint new pairs of outcome tokens.

By carefully analyzing the OrderFilled events for each participant, you can piece together the intricate logic of the exchange.

Start Your On-Chain Journey

You now have the foundational knowledge and the practical tools to start your journey as an on-chain analyst. The world of Polymarket’s data is vast and full of insights waiting to be discovered. So fire up your code editor, start exploring the blockchain, and begin decoding the digital tea leaves. The story of the markets is there for the taking—all you have to do is read it.