Examples
This page shows a minimal Solidity consumer that resolves a single player-prop market against a finalized Props Oracle assertion. It is intentionally stripped down — no access control, no fee math, no multi-market logic — to isolate the oracle interaction.
Minimal consumer contract
The pattern below locks a market at creation, then resolves it by checking the UMA assertion result and reading the UMA assertion log off-chain. On-chain, the consumer stores only what it needs to verify later: the canonical event ID, the oracle player ID, the stat type, and the locked line.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IOptimisticOracleV3 {
function getAssertionResult(bytes32 assertionId) external view returns (bool);
}
contract SimplePropConsumer {
IOptimisticOracleV3 public immutable oo;
struct Market {
bytes32 canonicalEventId;
bytes32 oraclePlayerId;
string statType; // e.g. "batter_hits"
int256 lockedLine; // scaled by 10 to keep half-points as integers
address bettor;
uint256 stake;
bool isOver;
bool resolved;
}
mapping(uint256 => Market) public markets;
constructor(address optimisticOracleV3) {
oo = IOptimisticOracleV3(optimisticOracleV3);
}
function canResolve(uint256 marketId, bytes32 assertionId) external view returns (bool) {
Market memory m = markets[marketId];
if (m.resolved) return false;
return oo.getAssertionResult(assertionId);
}
// resolveMarket takes the statValue the consumer parsed from the
// UMA AssertionMade log off-chain. The off-chain step also verifies
// the decoded claim matches m.canonicalEventId, m.oraclePlayerId,
// and m.statType.
function resolveMarket(uint256 marketId, bytes32 assertionId, int256 statValueX10) external {
Market storage m = markets[marketId];
require(!m.resolved, "Already resolved");
require(oo.getAssertionResult(assertionId), "Not truthful");
bool over = statValueX10 > m.lockedLine;
bool won = (over == m.isOver);
m.resolved = true;
if (won) {
payable(m.bettor).transfer(m.stake * 2);
}
}
}Two things are worth flagging. First, resolveMarket trusts whatever statValueX10 the caller passes in. In production you want to either require a signed proof from a trusted relayer, or pull the log data on-chain using an archival node or a light-client proof. The simplest production-ready path is to have the consumer's own backend watch for AssertionMade events, parse the JSON, and call resolveMarket with the value — the on-chain check still guarantees the UMA assertion resolved truthfully, which is the part that matters for dispute safety.
Second, the statType field here is a string for readability. In real contracts, hash it to bytes32 at market creation and store the hash, both to save gas and to avoid subtle issues with string comparison.
Off-chain parsing
The JavaScript side of the integration is a log filter. Pseudocode:
const logs = await oo.queryFilter(oo.filters.AssertionMade());
const claim = JSON.parse(ethers.toUtf8String(logs[0].args.claim));
if (claim.assertion_type !== "MARKET_RESOLUTIONS") throw new Error("wrong claim type");
if (claim.canonical_event_id !== canonicalEventId) throw new Error("wrong event");
const entry = claim.player_stats.find(
p => p.oracle_player_id === oraclePlayerId && p.stat_type === statType
);
const statValueX10 = Math.round(entry.stat_value * 10);
await consumer.resolveMarket(marketId, logs[0].args.assertionId, statValueX10);In production, verify the canonical event ID and oracle player ID locally before trusting the claim — recompute both hashes from the inputs you stored at market creation and compare. If either does not match, something is wrong and the resolution should be rejected.
Dispute awareness
If a MARKET_RESOLUTIONS assertion is disputed and the disputer wins, UMA marks that assertion untruthful and the consumer should continue polling for a corrected assertion. The relayer, or any party, can repost corrected values after a fresh validation cycle. Markets should not pay out and should not refund on a lost dispute; they should simply wait for the next truthful assertion.