kronos-sdk
v0.6.0
Published
TypeScript SDK for the Kronos automation protocol on TON — register recurring on-chain jobs, run automatons, decode events.
Maintainers
Readme
kronos-sdk
TypeScript SDK for the Kronos automation protocol on TON — Chainlink Automation for the TON ecosystem.
Register recurring on-chain jobs, decode events, maintain a local automaton mirror. Works in browsers, Node servers, and TON sandbox tests.
npm install kronos-sdk @ton/core
npx kronos-sdk init # installs .claude/skills/ so Claude/Cursor know Kronos@ton/core is a peer dependency — bring your own version (>= 0.63.0). @ton/sandbox and forgeton-sdk are optional peers — only needed when you use kronos-sdk/testing or run automaton flows.
💡 Running an automaton? Install
forgeton-sdkalongside this one. Pool-side operations (stake, slash, pool events) live there. This SDK covers the Kronos registry only.
🤖 Using Claude Code / Cursor? Run
npx kronos-sdk initonce — it installs the Kronos skills (/kronos-register-job,/kronos-target-receiver,/kronos-scaffold, …) into your project's.claude/skills/. Every future AI session in this project knows how to use Kronos without needing prompt engineering.
Three layers, pick what you need
┌──────────────────────────────────────────────────────┐
│ KronosClient registerJobOpts decodeEvent │ high-level
├──────────────────────────────────────────────────────┤
│ executionEconomics assignedAutomatonIndex isDue │ pure helpers
├──────────────────────────────────────────────────────┤
│ KronosRegistry │ contract wrapper (1:1 ABI)
└──────────────────────────────────────────────────────┘You can drop down any layer — they're all part of the public surface.
Quickstart
Register a recurring job
import { TonClient, WalletContractV4, internal } from '@ton/ton';
import { mnemonicToPrivateKey } from '@ton/crypto';
import { Address, beginCell, toNano } from '@ton/core';
import { registerJobOpts, KronosClient, KronosRegistry } from 'kronos-sdk';
const REGISTRY_ADDRESS = Address.parse('EQ...');
const TARGET = Address.parse('EQ...');
const tonClient = new TonClient({ endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC' });
const registry = tonClient.open(KronosRegistry.createFromAddress(REGISTRY_ADDRESS));
const client = new KronosClient({ registry });
const cfg = await client.jobs.config();
const opts = registerJobOpts(
{
target: TARGET,
message: beginCell().storeUint(0, 32).endCell(),
interval: 3600, // once per hour
reward: toNano('0.05'), // automaton reward
gasLimit: toNano('0.02'), // forwarded gas
maxExecutions: 0, // unlimited (default)
windowBefore: 30, // tolerate 30s early
windowAfter: 600, // and 10min late
},
cfg, // auto-funds 100 runs
);
await client.jobs.register(walletSender, opts);Decode registry events from your indexer
import { decodeEvent } from 'kronos-sdk';
// Each `Cell` is the body of an external-out message emitted by the registry.
// For pool-side events (AutomatonRegistered, AutomatonSlashed, …) use
// `decodeEvent` from forgeton-sdk on the pool's external bodies.
for (const body of externalBodies) {
const ev = decodeEvent(body);
if (ev === null) continue; // not a Kronos event — try forgeton-sdk next
switch (ev.kind) {
case 'JobExecuted':
console.log(`job ${ev.jobId} run ${ev.executionCount} by ${ev.automaton}`);
break;
case 'AssignedAutomatonMissed':
console.warn(`automaton ${ev.assigned} missed window for job ${ev.jobId}`);
break;
}
}Run an automaton (staking via forgeton-sdk)
Pool operations live in forgeton-sdk. Use both SDKs together:
import { KronosClient, KronosRegistry } from 'kronos-sdk';
import { ForgeTON } from 'forgeton-sdk';
const registry = tonClient.open(KronosRegistry.createFromAddress(REGISTRY_ADDRESS));
const pool = tonClient.open(ForgeTON.createFromAddress(FORGETON_ADDRESS));
const client = new KronosClient({ registry });
// Stake to become an automaton (pool-side; forgeton-sdk).
const poolCfg = await pool.getConfig();
const consumerCount = await pool.getConsumerCount();
await pool.sendRegisterAutomaton(walletSender, {
value: poolCfg.minStake
+ poolCfg.minGasForRegister
+ BigInt(consumerCount) * poolCfg.syncGasCost,
});
// Find the next due job and execute it (registry-side; kronos-sdk).
const jobCount = await client.jobs.count();
for (let i = 0n; i < jobCount; i++) {
if (!(await client.jobs.exists(i))) continue;
if (!(await client.jobs.isDue(i))) continue;
// Check if you're the assigned automaton for the primary window.
const assigned = await client.assignedAutomatonFor(i);
if (assigned && !assigned.equals(myAddress)) continue;
await client.jobs.execute(walletSender, { value: toNano('0.5'), jobId: i });
}Inspect a job's window state
import { jobWindowState, isDue, nextExecutionAt } from 'kronos-sdk';
const job = await client.jobs.get(jobId);
const cfg = await client.jobs.config();
const state = jobWindowState({
lastExecutedAt: job!.lastExecutedAt,
interval: job!.interval,
windowBefore: job!.windowBefore,
windowAfter: job!.windowAfter,
primaryWindowSeconds: cfg.primaryWindowSeconds,
expireAfter: job!.expireAfter,
});
// state.status: 'never-executed' | 'too-early' | 'primary' | 'fallback' | 'too-late' | 'expired'
// state.secondsToNext: countdown to next status changeReference
KronosClient
High-level façade over the registry. Construct with an already-opened contract handle:
const client = new KronosClient({
registry: tonClient.open(KronosRegistry.createFromAddress(REGISTRY_ADDRESS)),
});| Namespace | Methods |
|-----------|---------|
| client.jobs | register, execute, fund, ensureFunded, cancel, pause, resume, withdraw, update, updateFull, cleanupExpired, sweepDust, performHousekeeping, get, exists, isDue, nextDue, balance, balanceHealth, economics, count, config |
| client.mirror | snapshot, assignedFor, activeCount — registry-side reads against the dense automaton mirror maintained by inbound AutomatonSync |
| client.events | decode, decodeAll |
Plus convenience methods on the client itself: assignedAutomatonFor(jobId), windowFor(jobId).
For pool-side operations (stake, unstake, slash, pool events) use
forgeton-sdk'sForgeTONclass directly.
registerJobOpts(input, cfg?)
Helper that fills in sensible defaults (30s before / 10min after window, no expiry, unlimited runs) and, if cfg is supplied, auto-computes funding for 100 runs.
registerJobOpts(
{
target,
message,
interval: 3600,
reward: toNano('0.05'),
gasLimit: toNano('0.02'),
// Optional overrides — omit for defaults:
maxExecutions: 0, // unlimited
windowBefore: 30,
windowAfter: 600,
expireAfter: 0, // never
funding: toNano('1'), // or omit + pass cfg to auto-compute
},
cfg,
);decodeEvent(body: Cell): KronosEvent | null
Parses an external-log message body into a typed registry event. The discriminant is event.kind. Returns null when the opcode is not a Kronos event (try forgeton-sdk's decodeEvent next).
All registry event kinds:
JobRegistered JobExecuted JobFunded JobCancelled JobUpdated
JobPaused JobResumed JobWithdrawn JobExpired JobDustSwept
JobHousekeepingExecuted ForgetonSet AutomatonMirrorUpdated AssignedAutomatonMissed
ConfigUpdated TreasuryUpdated HousekeepingJobSet FeesWithdrawn
UpgradeProposed UpgradeCancelled CodeUpdated SlashRetriedPool-side events (AutomatonRegistered, AutomatonSlashed, …) are decoded by forgeton-sdk's own decodeEvent. Chain both decoders if you're watching both contracts.
Pure helpers
| Function | Purpose |
|----------|---------|
| executionEconomics({ reward, gasLimit, protocolFeeBps }) | Returns { totalCost, protocolFee, automatonReward, gasLimit }. Mirrors on-chain JobConfig.executionEconomics. |
| recommendedRegisterValue(...) | max(executions * totalCost, minFunding) + minGasReserve. The number you attach to RegisterJob. |
| previewJobCost({ reward, gasLimit, interval, executions? }, cfg) | One-call UI-friendly preview — { perExecutionCost, minFunding, recommended, protocolFee, burnRatePerDay, runsAtRecommended, … }. |
| estimateJobGas({ initCode, initData?, body }) or estimateJobGas({ blockchain, target, body }) | Sandbox-based gas estimator — { gasUsed, recommended } with 20% buffer. initCode mode deploys a fresh account; target mode needs the contract pre-deployed on the supplied blockchain. Requires @ton/sandbox (optional peer). |
| assignedAutomatonIndex({ jobId, executionCount, activeAutomatonCount }) | The on-chain assignment formula (jobId + execCount) % activeCount. |
| resolveAssignedAutomaton({ ..., automatonAddresses }) | Same, but resolves to an Address using a mirror snapshot. |
| jobWindowState({ lastExecutedAt, interval, ... }) | Full window inspection: status, deadlines, secondsToNext. |
| isDue(...), nextExecutionAt(...) | Convenience over jobWindowState — time/expiry only. |
| isExecutable({ ..., balance, perExecutionCost, isActive }) | Faithful preview of Execute success — also checks active + funded. Use this in automaton daemons. |
| validateRegisterOpts(opts, cfg) | Pre-flight foot-gun check: throws on contract-reject values (bad interval / reward / gasLimit / expireAfter), returns warnings[] for likely-buggy-but-legal inputs. Wired into registerJobOpts automatically when cfg is supplied — opt out with { validate: false }. |
JobPresets
Three spread-in profiles that collapse the windowBefore/windowAfter choice to a name:
import { registerJobOpts, JobPresets } from 'kronos-sdk';
// Every minute, strict window.
registerJobOpts({ ..., interval: 60, ...JobPresets.tight }, cfg);
// Hourly (current defaults).
registerJobOpts({ ..., interval: 3600, ...JobPresets.default }, cfg);
// Daily / weekly, tolerant of late runs.
registerJobOpts({ ..., interval: 86_400, ...JobPresets.loose }, cfg);JobWatcher
Long-running subscription for a single job's events — polls the registry tx stream, filters by jobId, emits typed callbacks. Plug in any transaction source (TonClient, custom indexer, sandbox for tests):
import { JobWatcher } from 'kronos-sdk';
const watcher = new JobWatcher(client, jobId, { source: myEventSource });
watcher
.on('JobExecuted', (ev) => console.log(`run ${ev.executionCount} by ${ev.automaton}`))
.on('JobFunded', (ev) => console.log(`topped up by ${ev.amount}`))
.on('LowBalance', (ev) => alertOps(ev.jobId, ev.runsRemaining));
const stop = watcher.start();
// ... later ...
await stop();Test harness — kronos-sdk/testing
Shrinks "deploy registry + register job + fast-forward + execute" to a one-liner. Requires @ton/sandbox installed:
import { SandboxKronos } from 'kronos-sdk/testing';
const kronos = await SandboxKronos.deploy(blockchain, { owner, treasury, via: owner.getSender() });
const jobId = await kronos.register({ target, message, interval, reward, gasLimit, via });
kronos.fastForward(301);
const res = await kronos.executeNext(jobId, automaton.getSender());See /kronos-integration-test for the full API + patterns.
Error introspection
import { explainError, formatErrorExplanation, ERR } from 'kronos-sdk';
const e = explainError(132);
// { code: 132, origin: 'kronos', name: 'NotJobOwner',
// message: 'Operation requires the job owner.',
// relatedSkill: '/kronos-job-lifecycle' }
// Single-line for logs or thrown Error messages.
throw new Error(formatErrorExplanation(e));
// → Error: [NotJobOwner] (132) Operation requires the job owner. See /kronos-job-lifecycleCovers every E_* from contracts/errors.tolk plus common TVM exit codes. Pool-side codes (160-199) route to origin: 'forgeton' with a pointer — install forgeton-sdk and call its explainError for the prose.
Bundled compiled contract
import { loadRegistryCode, KronosRegistry } from 'kronos-sdk';
const code = loadRegistryCode();
const registry = KronosRegistry.createFromConfig({ owner, treasury }, code);
// → registry.init.code (BoC) + registry.address (deterministic from init)Ships with the compiled KronosRegistry.compiled.json under artifacts/. For the pool's BoC (ForgeTON.compiled.json), install forgeton-sdk and call its loadForgetonCode().
Examples
See examples/ for full programs:
01-register-job.ts— register a recurring counter-bumper job.02-automaton-register.ts— run an automaton (uses both kronos-sdk + forgeton-sdk).03-decode-events.ts— minimal indexer that prints every registry event.
Working with AI tools
This package ships AI-friendly assets alongside the runtime:
AGENTS.md— terse map of the SDK auto-discovered by Claude Code, Cursor, and other AI editors that follow the agents.md convention. Drop the SDK into your project and your assistant has context.skills/— drop-in.claude/skills/*.mdfiles for Claude Code:Job owner workflow
/kronos-register-job— guided job builder/kronos-job-lifecycle— update / pause / withdraw / cancel/kronos-monitor-events— indexer / alerting recipes
Contract dev workflow (writing the Kronos target)
/kronos-target-receiver— idiomatic Tolk receiver pattern/kronos-gas-sizing— empirically sizegasLimitwith a 20% buffer/kronos-integration-test—SandboxKronosharness patterns/kronos-scaffold— one-shot "bolt Kronos onto my contract"
Operator workflow
/kronos-automaton-setup— run an automaton (uses both SDKs)/kronos-deploy— self-host deployment + housekeeping setup
Debugging / admin
/kronos-debug-exit-code— exit-code triage/kronos-owner-ops— admin operations + timelocked upgrades
(Pool-side skills — stake an automaton, slash, admit a consumer — ship with
forgeton-sdk.)Install all of them in one step:
npx kronos-sdk initThis also prints a CLAUDE.md fragment you can paste into your project's instructions so the AI has day-one context.
JSDoc
@exampleblocks on every public method — your IDE's hover popups show runnable snippets.
Versioning
This SDK ships its own compiled BoC under artifacts/ (regenerated on each prepublishOnly). When the on-chain ABI changes, bump the SDK's major version. While we're pre-mainnet (v0.x), expect breaking changes alongside contract upgrades.
License
MIT.
