import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
Quick Start
In the next five minutes you will: post a question to OMY, propose an answer with a bond, and react to the resolved truth in your own contract.
:::tip This guide assumes
- A TON wallet with some TON for gas
- A USDT jetton wallet for posting bonds
- Your own consumer contract on TON that will receive the result callback :::
1 — Install the SDK
The SDK is one file you copy into your project:
mkdir -p src/omy
curl -L https://raw.githubusercontent.com/Omy-network/omy/main/sdk/index.ts \
-o src/omy/index.ts
npm i @ton/core @ton/ton
That's it. No build step, no peer-dep gymnastics.
2 — Contract addresses
Network-specific contract addresses ship at launch. Plug them into your environment (see Installation → Environment variables).
3 — Ask a question
The flow: you send CreateAssertion to the OracleFactory. The factory
deploys a child Assertion contract (one per question), discovers + binds its
bond jetton wallet via TEP-89, and replies to you with AssertionCreated{id, assertion}.
import {Address, beginCell, toNano} from '@ton/core';
import {buildMeta, buildCreateAssertion} from '../omy';
import {FACTORY, RESOLVER} from '../config'; // your env-bound addresses
const meta = buildMeta({
identifier: 1n, // your data identifier
factTimestamp: Math.floor(Date.now() / 1000), // when the fact occurred
callbackRecipient: myConsumerContract, // where OracleResult lands
question: beginCell()
.storeStringTail('Did event X happen by 2026-06-01?')
.endCell(),
});
const body = buildCreateAssertion({
id: 1n,
resolver: RESOLVER,
bondAmount: 1_000_000n, // 1 USDT (6 decimals)
liveness: 3600, // 1-hour challenge window
meta,
});
// Send `body` to FACTORY with value = toNano('0.6')
// — enough TON for: deploy + TEP-89 round-trip + SetJettonWallet
import {TonConnectUI} from '@tonconnect/ui';
const tonConnect = new TonConnectUI({...});
await tonConnect.sendTransaction({
validUntil: Math.floor(Date.now() / 1000) + 60,
messages: [{
address: FACTORY.toString(),
amount: toNano('0.6').toString(),
payload: body.toBoc().toString('base64'),
}],
});
:::info What just happened?
- The factory accepted the message + paid for the
Assertiondeploy - The factory asked the USDT jetton master "what's the assertion's wallet address?" (TEP-89)
- The master replied; factory then sent
SetJettonWalletto the new assertion - Factory notified your address with
AssertionCreated{id, assertion}— that's where you find the spawned assertion's address
The whole round-trip typically takes 6-10 seconds. :::
4 — Propose an answer (with a bond)
Anyone can propose. Send a TEP-74 jetton transfer to the assertion's address
with an inline op (0x01 = propose-true) and an amount of
bondAmount + protocolFee.
import {buildBondPayload, OP_PROPOSE_TRUE} from '../omy';
const payload = buildBondPayload(OP_PROPOSE_TRUE);
// Send via your USDT wallet's AskToTransfer:
// amount = 3_000_000n (= 1 USDT bond + 2 USDT fee, Phase A)
// destination = assertionAddress (from AssertionCreated)
// forwardPayload = payload
// forwardTon = 0.05 TON (so the assertion receives the notification)
// value = 0.2 TON (gas for the intake + fee forward)
The assertion will:
- Verify the amount covers
bondAmount + protocolFee - Store
proposerBond = msg.amount - protocolFee - Forward
protocolFeeto the factory's bound treasury - Transition status to Proposed with
deadline = now + liveness
The Phase A protocolFee is small (~2 USDT-units). See the
Reference for current values.
:::caution Underfunded? You'll get refunded
If you send less than bondAmount + protocolFee, the assertion does
not throw. It refunds your full transfer and stays in Open status. This
is the "M-1 refund-and-return" pattern — funds can never strand on a guard.
:::
5 — Handle the resolution callback
Once finalize() (no dispute) or Resolve (after dispute) fires, the
assertion sends OracleResult{questionId, answer} to your callbackRecipient.
import "@omy/oracle-messages" // shared opcodes: OracleResult, OracleDisputed
struct (0x0a02) OracleResult {
questionId: uint64
answer: bool
}
fun onInternalMessage(in: InMessage) {
val msg = lazy MyMessage.fromSlice(in.body);
match (msg) {
OracleResult => {
// ⚠️ Delivery is at-least-once. ALWAYS dedupe by questionId.
var st = Storage.load();
if (st.settled.contains(msg.questionId)) {
return; // already processed — drop quietly
}
st.settled.set(msg.questionId, true);
st.save();
// Settle YOUR contract based on msg.answer
if (msg.answer) {
payoutWinners();
} else {
refundHolders();
}
}
// ... your other handlers ...
}
}
import {parseOracleResult} from '../omy';
// In your message handler (e.g., subscribed to your consumer's inbound traces):
const parsed = parseOracleResult(msg.body);
if (parsed) {
// parsed.questionId : bigint
// parsed.answer : boolean
console.log(`Question ${parsed.questionId} resolved to ${parsed.answer}`);
// Dedupe in your DB before acting
if (await alreadySettled(parsed.questionId)) return;
await markSettled(parsed.questionId);
await settleBet(parsed.questionId, parsed.answer);
}
:::danger Always dedupe
OracleResult may be delivered more than once: anyone can pay gas to call
ReemitResult and re-trigger the callback (covers the case where the first
delivery hit a bounced message). Your consumer MUST dedupe by questionId
— otherwise an attacker can trigger double payouts at the cost of 0.02 TON.
This is the single most common integration bug. See Patterns → Dedupe. :::
What you just built
┌──────────────────┐
│ Your consumer │
│ contract │
└────────┬─────────┘
│
propose() ─┴─► Assertion ──── liveness ─┐
│ │
│ (rare) dispute │ (common) finalize
▼ ▼
Resolver ──► OracleResult ──► You
│
you dedupe + settle
Where to go from here
import {DocCards, DocCard} from '@site/src/components/docs/DocCards';