Skip to main content

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';

Watch the chain for Proposed assertions; once their deadline elapses, call finalize(). Permissionless — anyone can run one. Earns nothing directly but keeps your markets liquid. Watch for proposed answers; cross-check against the question's specified source; dispute if the answer is wrong. Earns from the loser's bond cut on successful disputes. Subscribe to all assertion events; populate a database with full lifecycle history. Powers UIs, analytics, settlement. For disputed assertions that exceed the arbitration window, anyone can call TimeoutRefund to recover both bonds. Keeper makes sure this happens promptly.

Finalize keeper — reference shape

The simplest possible keeper: scan for Proposed assertions, finalize when ready.

src/keepers/finalize.ts
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.

src/keepers/disputer.ts
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.

src/indexer/scan.ts
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:

scripts/deploy-my-consumer.tolk
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:

check assertion status
acton rpc info <assertion-address> | grep status

# → status: 3 (RESOLVED)
trace a tx end-to-end
acton rpc trace <tx-hash>
check factory monetization
acton rpc run-method <factory> protocolFee

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