Skip to main content

Concepts

Before you write integration code, the four ideas below are worth ten minutes of your time. They make everything else in the SDK obvious.

Assertion

An assertion is a single yes/no claim about the world, bound to a specific question and resolver, with collateral attached. One assertion = one on-chain contract, deployed per question by the OracleFactory.

Question: "Did event X happen by date Y?"
Asserter: @user_who_proposed
Answer: true
Bond: 1 USDT
Deadline: in 1 hour (if undisputed → accepted)
Status: Proposed

Assertions are immutable. Once spawned, their config — bond size, liveness window, resolver address, monetization snapshot — never changes. You can't "update" an assertion; if you want different terms, you spawn a new one.

Lifecycle

Every assertion is a tiny state machine with five states:

┌────────────┐
│ Open │ ← initial state after deploy
└─────┬──────┘
propose() │

┌────────────┐
│ Proposed │ ← answer is on-chain, challenge window open
└──┬──────┬──┘
finalize() │ │ dispute()
(no dispute) │ │
▼ ▼
┌──────────┐ ┌────────────┐
│ Resolved │ │ Disputed │ ← someone challenged
└──────────┘ └─────┬──────┘
│ Resolve from resolver
│ or TimeoutRefund after window

┌──────────┐ ┌─────────────┐
│ Resolved │ │ Cancelled │
└──────────┘ └─────────────┘

Three terminal states:

  • Resolved (Finalize) — common case. Proposer was unchallenged, gets bond back, consumer notified.
  • Resolved (Resolve) — dispute path. Resolver picked a winner; loser bond split 50/50 between winner and treasury.
  • Cancelled — arbitration timed out. Both bonds refunded; consumer notified that the question went unanswered.

Your consumer needs to handle all three terminal outcomes — see Patterns.

Bond

The bond is refundable collateral that the proposer locks at propose(). It's there for one reason: make lying expensive.

Bond is...Used for
ProposerbondAmount + protocolFeebond is refunded if right; fee is burned
DisputerbondAmount (no fee)bond is refunded if right; lost if wrong

In the dispute path, the loser's bond gets split 50/50 between the winner and the treasury. So if you propose, lose, and your bond was 1 USDT:

  • You posted 1 USDT bond + 2 USDT fee at propose time → 3 USDT out of pocket
  • The fee (2 USDT) was burned at propose; you never see it again
  • The bond (1 USDT) goes 0.5 to the disputer, 0.5 to treasury
  • Net: you lose 3 USDT, disputer earns 0.5 USDT + their own bond back, treasury earns 2.5 USDT

The math is asymmetric on purpose. Disputer-watcher bots will only bother to dispute when they're highly confident. Proposers only bother to lie when they're so confident they'll win against any disputer — which is exactly the problem the bond is designed to make irrational.

:::tip Bond sizing For consumer contracts that auto-request resolution (e.g., prediction markets), the bond should scale to TVL. Our reference Market contract clamps the bond to 2% of total collateral, floored at the configured minimum and capped at 10 000 USDT. See tolk/contracts/Market.tolk RequestResolution. :::

Callbacks

Your consumer contract is a callback recipient on the assertion. OMY sends you exactly two message opcodes:

import {DocFeatures, DocFeature} from '@site/src/components/docs/DocFeatures';

Sent when the assertion reaches Resolved — either via finalize() (common) or Resolve (dispute path). Carries {`{ questionId, answer }`}. Sent when the assertion enters Disputed status. Lets you pause your consumer logic if you want to wait for resolution. Carries {`{ questionId }`}. Optional to handle.

That's the whole interface from OMY → you. Two messages, both with a predictable shape.

At-least-once delivery

This is the single most important integration property to internalize:

:::danger Callbacks may fire more than once OracleResult delivery is at-least-once, not exactly-once. ReemitResult is a permissionless message that any address can send to a resolved assertion to re-trigger the callback. Use case: the original delivery hit a bounced message or your consumer rejected it; someone pays gas to retry.

Your consumer MUST dedupe by questionId. Otherwise an attacker can trigger double-credit at gas cost. See Patterns → Dedupe. :::

The dedupe pattern is two lines of Tolk:

if (st.settled.contains(msg.questionId)) return;
st.settled.set(msg.questionId, true);

Always do this. Even if you're sure your consumer is the only target — assume the message will land twice and design for it.

The four things to remember

import {DocCards, DocCard} from '@site/src/components/docs/DocCards';

What's next

  • Tutorial — apply these concepts in a 30-minute walk-through.
  • Patterns — production-ready snippets for the gotchas above.
  • Reference — every export, opcode, and constant.