Tooling
The off-chain infrastructure your consumer needs to run reliably: keeper bots, indexers, deploy scripts, and operational monitoring. This page is the field guide for everything around the SDK.
Keeper bots
TON has no on-chain cron. A Finalize call on an unchallenged assertion has
to be sent by someone after the liveness window elapses. That someone is
a keeper bot.
import {DocFeatures, DocFeature} from '@site/src/components/docs/DocFeatures';
Finalize keeper — reference shape
The simplest possible keeper: scan for Proposed assertions, finalize when ready.
import {TonClient, WalletContractV4, internal} from '@ton/ton';
import {Address, beginCell, toNano} from '@ton/core';
import {OP_FINALIZE} from '../omy';
const client = new TonClient({
endpoint: process.env.TON_RPC_ENDPOINT!,
apiKey: process.env.TONCENTER_API_KEY!,
});
async function finalizeIfReady(assertionAddr: Address) {
const {stack: statusStack} = await client.runMethod(assertionAddr, 'status');
const status = statusStack.readNumber();
if (status !== 1) return; // STATUS_PROPOSED
const {stack: deadlineStack} = await client.runMethod(assertionAddr, 'deadline');
const deadline = deadlineStack.readNumber();
const now = Math.floor(Date.now() / 1000);
if (now < deadline) return;
// Ready — send Finalize
const body = beginCell().storeUint(OP_FINALIZE, 32).endCell();
await sendMessage(assertionAddr, body, toNano('0.1'));
}
async function main() {
const assertions = await loadAssertionsFromDb(); // your indexer feeds this
for (const a of assertions) {
try { await finalizeIfReady(a); }
catch (e) { console.error(`finalize failed for ${a}:`, e); }
}
}
setInterval(main, 60_000);
In production, this keeper should:
- Run as a long-lived process, not cron (more responsive)
- Track which assertions it's already attempted (avoid double-sends)
- Back off on RPC errors (TonCenter rate limits)
- Log every send with tx hash for traceability
- Run multiple instances behind a leader election (you don't want two keepers fighting)
A reference implementation lives at
keeper/ in the repo —
currently minimal-viable, production-grade refactor on the roadmap.
Dispute watcher
The economic engine of the dispute path. The watcher monitors proposed answers, cross-references against the question's data source, and disputes wrong answers.
async function checkAndDispute(assertion: AssertionState) {
// 1. Fetch the question text from assertion's Meta
const question = await fetchQuestion(assertion.address);
// 2. Parse the question template — extract source + threshold
const template = parseTemplate(question);
// 3. Query the source independently
const truth = await querySource(template);
// 4. Compare to proposed answer
if (truth !== assertion.proposedAnswer) {
console.log(`Dispute opportunity: ${assertion.address} proposed ${assertion.proposedAnswer}, truth is ${truth}`);
await postDispute(assertion.address, assertion.bondAmount);
}
}
The watcher's economics: posting a dispute bond is risk. If you're wrong, you lose half your bond to the proposer + half to treasury. Watcher bots should:
- Require very high confidence (95%+) before disputing
- Cross-reference multiple data sources, not just one
- Cap their exposure per period (don't run out of capital on a series of wrong calls)
- Track P&L over time — watcher economics are tight
Indexer
The indexer is the foundation everything else builds on. It scans the chain for OMY-related transactions and populates a database that powers UIs, analytics, keepers, and any settlement logic that needs history.
async function scanFactory() {
const txs = await client.getTransactions(FACTORY_ADDRESS, {limit: 100});
for (const tx of txs) {
const outMessages = tx.outMessages.values();
for (const msg of outMessages) {
const opcode = msg.body.beginParse().loadUint(32);
if (opcode === OP_ASSERTION_CREATED) {
const parsed = parseAssertionCreated(msg.body);
if (parsed) {
await db.recordAssertion({
id: parsed.id,
address: parsed.assertion.toString(),
spawnedAt: tx.now,
spawnTxHash: tx.hash().toString('hex'),
});
}
}
}
}
}
A real indexer needs:
- Block-by-block progression (don't drop or double-process)
- Reorg tolerance (TON re-orgs are rare but possible)
- Multiple opcode handlers per contract type
- Status getter polling for assertions you're tracking
- A sync cursor in the database
Deploy scripts
OMY uses Acton for deployments. Reference scripts in
tolk/scripts/.
Deploy your consumer
Your consumer follows the same pattern. Example for a Market deploy:
import "@acton/env"
import "@acton/emulation/scripts"
import "@contracts/market-storage"
import "@wrappers/Market.gen"
fun main() {
val deployer = scripts.wallet(env<string>("DEPLOYER") ?? promptWallet("deployer"));
val factory = env<address>("FACTORY") ?? promptAddress("OracleFactory address");
val resolver = env<address>("RESOLVER") ?? promptAddress("Resolver address");
val minter = env<address>("MINTER") ?? promptAddress("USDT minter");
val oracleParams = OracleParams {
factory, resolver,
oracleBond: ton("100"),
oracleLiveness: 3600,
question: beginCell().storeStringTail("Will event X happen?").endCell(),
};
val market = Market.fromStorage({
config: MarketConfig {id: 1, creator: deployer.address, oracle: oracleParams.toCell()}.toCell(),
state: MarketState {pool: MarketPool {}.toCell()}.toCell(),
yesBalance: [], noBalance: [],
});
println("MARKET ADDRESS={}", market.address);
val d = market.deploy(deployer.address, { value: ton("0.5") });
d.waitForFirstTransaction();
// Then: bind wallet, set oracle, ready for trading
}
Run with:
DEPLOYER=deployer FACTORY=$TONOMY_FACTORY acton script scripts/deploy-my-consumer.tolk
Production deploy hygiene
A production deploy is one-shot. OMY contracts have no upgrade path — v2 means a new address and a migration.
Before going live:
- Multisig signer for the deploy wallet (not a single key)
- Pre-launch smoke against the live factory with the exact same script
- Address pre-computed and verified in advance (offline check that your storage layout matches the factory's expectations)
- All env vars (FACTORY, RESOLVER, MINTER) double-checked — wrong factory address = your consumer doesn't work
- Monitoring + alerts ready to fire before the first user interacts
Monitoring
Three categories of metrics every OMY consumer should track:
On-chain state
- Consumer's status counter (Open / Awaiting / Resolved counts)
- Consumer's TON balance (don't run out of gas)
- Consumer's USDT balance (jetton wallet)
- Last activity timestamp (alert if too long since last tx)
Cross-contract
- Time-to-finalize on Proposed assertions (alert if drifting)
- Dispute rate (sudden spike = attempted manipulation)
- Cancelled assertion rate (factory issues, source unreachable)
Off-chain
- Indexer sync lag (latest block vs current chain head)
- Keeper bot uptime
- RPC error rate
- Source-data API uptime (if your questions reference external APIs)
A simple Prometheus + Grafana stack covers all of this. Reference dashboards will ship before mainnet.
CLI helpers
We don't ship a dedicated OMY CLI — Acton's scripts cover the cases. But useful one-liners for ops:
acton rpc info <assertion-address> | grep status
# → status: 3 (RESOLVED)
acton rpc trace <tx-hash>
acton rpc run-method <factory> protocolFee
Related
import {DocCards, DocCard} from '@site/src/components/docs/DocCards';