Skip to content

Tracking Price ​

ParameterDescriptionFormat
price (P)Center/reference price of tokenX in tokenY1e24 precision
alphaConcentration factorBPS (10000 = 1.0)
feeHbpsTrading feeHundredths of a basis point (1e6 = 100%)
expiryWhen the data expiresUnix timestamp

This guide explains how pricing works in OraclePool and how to track price changes. The key insight is that the execution price is not simply the oracle price — it's computed from both oracle data AND pool balances.

The Pricing Model ​

OraclePool uses a Concentrated Liquidity with Asymmetric Pricing (CLAP) model (an extension of the simpler CLCP curve). The price is determined by two components.

Oracle Data (Set by Operator) ​

The oracle operator sets these parameters:

ParameterDescriptionFormat
price (P)Center/reference price of tokenX in tokenY1e24 precision
alphaConcentration factorBPS (10000 = 1.0). Valid range: (10000, 65535]
feeHbpsTrading feeHundredths of a basis point (1e6 = 100%)
expiryWhen the data expires (exclusive)Unix timestamp (40-bit)

The alpha parameter defines the price range: [P/alpha, P*alpha]

For example, with alpha = 10100 (1.01 in BPS):

  • If oracle price P = 1.0, the actual price can range from ~0.99 to ~1.01

Pool Balances ​

The pool's tokenX / tokenY balances determine where within the price range the actual price sits. There is a single live quantity per token — the pool does not distinguish "reserves designated for trading" from "balances held"; everything held by the pool participates in pricing.

CLAP splits each swap into two phases:

  1. Stable phase. If the trade brings the pool closer to 50/50 value (P · X ≈ Y), the improving portion executes at the flat oracle price P.
  2. Curve phase. Any remaining input swaps along the concentrated-liquidity x·y = k curve using virtual reserves derived from real balances plus α.

Worsening trades skip the stable phase and execute entirely on the curve.

INFO

Oracle price alone is NOT the execution price. To compute the true price for a given trade size, use getQuote(...) on the pool (or call the oracle's getQuote(...) directly with the current balances). For a marginal bid/ask spread without an amount, use getCurrentPrice(key, balanceX, balanceY).

Reading Oracle Data ​

Computing the Oracle Key ​

Oracle data is stored by a key derived from the token pair:

solidity
bytes32 key = keccak256(abi.encodePacked(tokenX, tokenY));

WARNING

Order matters! tokenX must come first when computing the key.

Getting the Data ​

solidity
// On the oracle contract (get oracle address from pool.getOracle())
function getData(bytes32 key)
    external view
    returns (uint256 price, uint256 feeHbps, uint256 alpha, uint256 expiry);

Note: getData does not check expiry. To get just the price with expiry enforcement, call getPrice(key) instead.

Example usage:

solidity
address oracle = pool.getOracle();
(address tokenX, address tokenY) = pool.getTokens();

bytes32 key = keccak256(abi.encodePacked(tokenX, tokenY));
(uint256 price, uint256 feeHbps, uint256 alpha, uint256 expiry) = IClapOracle(oracle).getData(key);

Getting the Current Price ​

The deployed oracle is ClapOracle. It exposes a bid/ask spread rather than a single mid price:

solidity
function getCurrentPrice(bytes32 key, uint256 reserveX, uint256 reserveY)
    external view
    returns (uint256 bidPriceE24, uint256 askPriceE24);

The oracle price P is always one of the two bounds; the other is the curve-implied price derived from the current pool balances. Both are 1e24-scaled.

How getCurrentPrice works — step 1 ​

Reads and validates (non-expired) the oracle data for key (price P, alpha).

How getCurrentPrice works — step 2 ​

Uses the provided balances as reserveX / reserveY.

How getCurrentPrice works — step 3 ​

Computes the curve-implied price from the balances and pairs it with P. The smaller of the two becomes the bid; the larger becomes the ask.

How getCurrentPrice works — step 4 ​

Returns (bidPriceE24, askPriceE24) with 1e24 precision.

Example:

solidity
address oracle = pool.getOracle();
(address tokenX, address tokenY) = pool.getTokens();
(uint256 balanceX, uint256 balanceY) = pool.getBalances();

bytes32 key = keccak256(abi.encodePacked(tokenX, tokenY));
(uint256 bidPriceE24, uint256 askPriceE24) =
    IClapOracle(oracle).getCurrentPrice(key, balanceX, balanceY);

// Prices are tokenX denominated in tokenY with 1e24 precision
// humanPrice = priceE24 / 1e24 * 10^(decimalsX - decimalsY)

For an executable quote on a specific trade size (which includes the CLAP stable-phase mechanics and fees), use pool.getQuote(swapXtoY, amountIn) — see the Making a Swap page.

Price Format and Decimal Adjustment ​

The price returned is with 1e24 precision and represents the price of tokenX in terms of tokenY.

To get a human-readable price, adjust for token decimals:

humanPrice = priceE24 / 1e24 * 10^(decimalsX - decimalsY)

Example — tokenX has 18 decimals and tokenY (USDC) has 6 decimals:

solidity
// getCurrentPrice returns: 18386714238 (1.838e10)
// decimalsX = 18, decimalsY = 6

// humanPrice = 1.838e10 / 1e24 * 10^(18-6)
//            = 1.838e10 / 1e24 * 1e12
//            = 1.838e10 / 1e12
//            = 0.01838
// This means 1 tokenX = 0.01838 USDC

Another example — tokenX has 6 decimals and tokenY has 18 decimals:

solidity
// Suppose getCurrentPrice returns: 5e34
// decimalsX = 6, decimalsY = 18

// humanPrice = 5e34 / 1e24 * 10^(6-18)
//            = 5e10 * 1e-12
//            = 0.05
// This means 1 tokenX = 0.05 tokenY

Tracking Price Changes ​

Price can change from two sources:

Balance Changes (Events Available) ​

Pool balances change on swaps, deposits and withdrawals:

solidity
event Swap(
    address indexed sender,
    address indexed recipient,
    bool indexed swapXtoY,
    uint256 actualAmountIn,
    uint256 amountOut,
    uint256 feeIn,
    uint256 feeOut
);

event Deposited(
    address indexed user,
    address indexed receiver,
    uint256 amountX,
    uint256 amountY,
    uint256 shares
);

event Withdrawn(
    address indexed user,
    address indexed receiver,
    uint256 shares,
    uint256 amountX,
    uint256 amountY
);

Notes:

  • actualAmountIn is the gross input including fee.
  • Only one of feeIn / feeOut is non-zero per swap: feeIn is in tokenIn for Y→X swaps; feeOut is in tokenOut (tokenY) for X→Y swaps. Both are forwarded to the FeeRewarder when set.

Oracle Data Changes (No Events) ​

DANGER

The oracle's setData() function does NOT emit events. Its signature is parameter-less — the price/fee/alpha/expiry payload is read directly from calldata (see src/libraries/Oracle.sol in the repo for the layout). To track oracle changes you must either watch transactions calling setData() on the oracle contract and decode the raw calldata, or periodically poll getData(key).

TypeScript Example ​

typescript
import { ethers } from 'ethers';

const ORACLE_POOL_ABI = [
    "function getOracle() view returns (address)",
    "function getTokens() view returns (address tokenX, address tokenY)",
    "function getBalances() view returns (uint256 totalX, uint256 totalY)",
    "event Swap(address indexed sender, address indexed recipient, bool indexed swapXtoY, uint256 actualAmountIn, uint256 amountOut, uint256 feeIn, uint256 feeOut)",
    "event Deposited(address indexed user, address indexed receiver, uint256 amountX, uint256 amountY, uint256 shares)",
    "event Withdrawn(address indexed user, address indexed receiver, uint256 shares, uint256 amountX, uint256 amountY)"
];

const CLAP_ORACLE_ABI = [
    "function getData(bytes32 key) view returns (uint256 price, uint256 feeHbps, uint256 alpha, uint256 expiry)",
    "function getCurrentPrice(bytes32 key, uint256 reserveX, uint256 reserveY) view returns (uint256 bidPriceE24, uint256 askPriceE24)"
];

const ERC20_ABI = [
    "function decimals() view returns (uint8)"
];

class PriceTracker {
    private pool: ethers.Contract;
    private oracle: ethers.Contract;
    private key: string;
    private decimalsX: number;
    private decimalsY: number;

    constructor(
        provider: ethers.Provider,
        poolAddress: string
    ) {
        this.pool = new ethers.Contract(poolAddress, ORACLE_POOL_ABI, provider);
    }

    async initialize() {
        // Get oracle and tokens
        const [oracleAddress, tokens] = await Promise.all([
            this.pool.getOracle(),
            this.pool.getTokens()
        ]);
        const [tokenX, tokenY] = [tokens.tokenX, tokens.tokenY];

        this.oracle = new ethers.Contract(
            oracleAddress,
            CLAP_ORACLE_ABI,
            this.pool.runner
        );

        // Compute key
        this.key = ethers.solidityPackedKeccak256(
            ['address', 'address'],
            [tokenX, tokenY]
        );

        // Get decimals for price formatting (convert BigInt to number)
        const tokenXContract = new ethers.Contract(tokenX, ERC20_ABI, this.pool.runner);
        const tokenYContract = new ethers.Contract(tokenY, ERC20_ABI, this.pool.runner);

        const [decX, decY] = await Promise.all([
            tokenXContract.decimals(),
            tokenYContract.decimals()
        ]);
        this.decimalsX = Number(decX);
        this.decimalsY = Number(decY);

        console.log(`Initialized tracker for ${tokenX}/${tokenY}`);
        console.log(`Oracle key: ${this.key}`);
    }

    async getCurrentPrice(): Promise<{
        bidPriceE24: bigint;
        askPriceE24: bigint;
        humanBid: number;
        humanAsk: number;
    }> {
        const { totalX, totalY }: { totalX: bigint; totalY: bigint } =
            await this.pool.getBalances();
        const [bidPriceE24, askPriceE24]: [bigint, bigint] =
            await this.oracle.getCurrentPrice(this.key, totalX, totalY);

        const decimalAdjustment = this.decimalsX - this.decimalsY;
        const toHuman = (p: bigint) =>
            parseFloat(p.toString()) / 1e24 * Math.pow(10, decimalAdjustment);

        return {
            bidPriceE24,
            askPriceE24,
            humanBid: toHuman(bidPriceE24),
            humanAsk: toHuman(askPriceE24),
        };
    }

    async getOracleData() {
        const [price, feeHbps, alpha, expiry]: [bigint, bigint, bigint, bigint] =
            await this.oracle.getData(this.key);
        return {
            price,
            feeHbps,
            alpha,
            expiry,
            expiryDate: new Date(parseInt(expiry.toString()) * 1000)
        };
    }
}

// Usage
async function main() {
    const provider = new ethers.JsonRpcProvider("https://rpc3.monad.xyz");

    const tracker = new PriceTracker(provider, "0x...poolAddress");
    await tracker.initialize();

    const { bidPriceE24, askPriceE24, humanBid, humanAsk } = await tracker.getCurrentPrice();
    console.log(`Bid / Ask (1e24): ${bidPriceE24} / ${askPriceE24}`);
    console.log(`Bid / Ask (human): ${humanBid} / ${humanAsk}`);

    const oracleData = await tracker.getOracleData();
    console.log('Oracle data:', oracleData);
}

Python Example ​

python
from web3 import Web3

def compute_oracle_key(token_x: str, token_y: str) -> bytes:
    """Compute the oracle data key for a token pair."""
    packed = bytes.fromhex(token_x[2:]) + bytes.fromhex(token_y[2:])
    return Web3.keccak(packed)

def get_current_price(w3: Web3, pool_address: str) -> dict:
    """Get the current bid/ask spread from an OraclePool."""

    pool_abi = [
        {"name": "getOracle", "type": "function", "inputs": [], "outputs": [{"type": "address"}]},
        {"name": "getTokens", "type": "function", "inputs": [],
         "outputs": [{"name": "tokenX", "type": "address"}, {"name": "tokenY", "type": "address"}]},
        {"name": "getBalances", "type": "function", "inputs": [],
         "outputs": [{"name": "totalX", "type": "uint256"}, {"name": "totalY", "type": "uint256"}]},
    ]

    oracle_abi = [
        {"name": "getData", "type": "function", "inputs": [{"type": "bytes32"}],
         "outputs": [{"type": "uint256"}, {"type": "uint256"}, {"type": "uint256"}, {"type": "uint256"}]},
        {"name": "getCurrentPrice", "type": "function",
         "inputs": [{"type": "bytes32"}, {"type": "uint256"}, {"type": "uint256"}],
         "outputs": [{"name": "bidPriceE24", "type": "uint256"}, {"name": "askPriceE24", "type": "uint256"}]},
    ]

    pool = w3.eth.contract(address=pool_address, abi=pool_abi)

    # Get pool info
    oracle_address = pool.functions.getOracle().call()
    token_x, token_y = pool.functions.getTokens().call()
    balance_x, balance_y = pool.functions.getBalances().call()

    # Compute key and get price
    oracle = w3.eth.contract(address=oracle_address, abi=oracle_abi)
    key = compute_oracle_key(token_x, token_y)

    bid_price_e24, ask_price_e24 = oracle.functions.getCurrentPrice(key, balance_x, balance_y).call()
    oracle_data = oracle.functions.getData(key).call()

    return {
        "token_x": token_x,
        "token_y": token_y,
        "balance_x": balance_x,
        "balance_y": balance_y,
        "bid_price_e24": bid_price_e24,
        "ask_price_e24": ask_price_e24,
        "oracle_price": oracle_data[0],
        "fee_hbps": oracle_data[1],
        "alpha": oracle_data[2],
        "expiry": oracle_data[3],
    }

# Usage
if __name__ == "__main__":
    w3 = Web3(Web3.HTTPProvider("https://rpc3.monad.xyz"))

    result = get_current_price(w3, "0x...poolAddress")

    print(f"Token X: {result['token_x']}")
    print(f"Token Y: {result['token_y']}")
    print(f"Balances: {result['balance_x']} / {result['balance_y']}")
    print(f"Bid / Ask (1e24): {result['bid_price_e24']} / {result['ask_price_e24']}")
    print(f"Oracle Price (1e24): {result['oracle_price']}")
    print(f"Alpha: {result['alpha']} BPS")

Key Takeaways ​

Price is a function of oracle data and pool balances ​

You need both to get the actual price. For trade-size-specific quotes use getQuote; for the marginal bid/ask spread use getCurrentPrice.

Oracle data has no events ​

setData() takes no parameters — the payload lives in the raw calldata. You must decode that calldata or periodically poll getData(key).

Balance changes have events ​

Watch Swap, Deposited and Withdrawn for balance-driven price changes.

Price precision is 1e24 ​

Use humanPrice = priceE24 / 1e24 * 10^(decimalsX - decimalsY)

Alpha defines the allowed range ​

Actual price is bounded by [P/alpha, P*alpha]. Alpha is in BPS where 10000 = 1.0; the valid on-chain range is (10000, 65535].


Agent Instructions: Querying This Documentation ​

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the ask query parameter:

GET /poe/tracking-price.md?ask=<question>

The question should be specific, self-contained, and written in natural language. The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.