Phase 1 — Monitor a Pool
A read-only agent that watches a Cetus trading pool. No funds at risk — it just observes price movement and simulates what a position would do.
Time: ~10 minutes | Risk: None (read-only)
What you’ll build
An agent that runs in a loop and:
- Connects to a Cetus pool on Sui
- Reads the current market price every 5 minutes
- Simulates a position and checks if it would need repositioning
- Logs everything to a JSON file
The code
Create agent.js:
import { initCetusSDK } from '@cetusprotocol/cetus-sui-clmm-sdk';
import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
import fs from 'fs';
import dotenv from 'dotenv';
dotenv.config();
const AGENT_ID = 'sui-cetus-yield';
const LOG_FILE = process.env.LOG_FILE || `${AGENT_ID}.log`;
const CHECK_INTERVAL = parseInt(process.env.CHECK_INTERVAL_MS || '300000');
const POOL_ID = process.env.CETUS_POOL_ID;
const THRESHOLD = parseInt(process.env.REBALANCE_THRESHOLD_TICKS || '100');
const RANGE = parseInt(process.env.POSITION_RANGE_TICKS || '200');
// --- Logging ---
function log(level, message, data = {}) {
const entry = { ts: new Date().toISOString(), agent: AGENT_ID, level, message, ...data };
const line = JSON.stringify(entry);
console.log(line);
try { fs.appendFileSync(LOG_FILE, line + '\n'); } catch {}
}
// --- Cetus SDK ---
const client = new SuiClient({ url: process.env.SUI_RPC || getFullnodeUrl('mainnet') });
const sdk = initCetusSDK({ network: process.env.NETWORK || 'mainnet' });
async function getPoolState() {
return await sdk.Pool.getPool(POOL_ID);
}
// --- Simulated Position ---
let simPosition = null; // { tickLower, tickUpper, center, openedAt }
function simulatePositionCheck(currentTick) {
if (!simPosition) {
// Open a simulated position centered on current price
const tickLower = currentTick - RANGE;
const tickUpper = currentTick + RANGE;
simPosition = { tickLower, tickUpper, center: currentTick, openedAt: new Date().toISOString() };
log('event', 'sim_position_opened', { tickLower, tickUpper, center: currentTick });
return;
}
const drift = Math.abs(currentTick - simPosition.center);
const outOfRange = currentTick < simPosition.tickLower || currentTick > simPosition.tickUpper;
if (outOfRange || drift > THRESHOLD) {
log('event', 'sim_drift_detected', { drift, threshold: THRESHOLD, currentTick });
// Simulate rebalance: close old, open new
simPosition = {
tickLower: currentTick - RANGE,
tickUpper: currentTick + RANGE,
center: currentTick,
openedAt: new Date().toISOString(),
};
log('event', 'sim_rebalance', { newTickLower: simPosition.tickLower, newTickUpper: simPosition.tickUpper });
} else {
log('info', 'Simulated position in range', { currentTick, drift, threshold: THRESHOLD });
}
}
// --- Main Loop ---
async function runAgent() {
log('info', 'Agent starting', { mode: 'monitor', pool: POOL_ID, checkInterval: CHECK_INTERVAL });
while (true) {
try {
const pool = await getPoolState();
const currentTick = pool.current_tick_index;
log('info', 'Cycle', {
mode: 'monitor',
tick: currentTick,
sqrtPrice: pool.current_sqrt_price,
});
simulatePositionCheck(currentTick);
} catch (error) {
log('error', 'Cycle failed', { error: error.message });
}
log('info', `Next check in ${CHECK_INTERVAL / 1000}s`);
await new Promise(r => setTimeout(r, CHECK_INTERVAL));
}
}
runAgent().catch(err => {
log('error', 'Fatal', { error: err.message });
process.exit(1);
});Run it
node agent.jsYou should see output like:
{"ts":"...","agent":"sui-cetus-yield","level":"info","message":"Agent starting","mode":"monitor","pool":"0xb8d7..."}
{"ts":"...","agent":"sui-cetus-yield","level":"info","message":"Cycle","mode":"monitor","tick":69758}
{"ts":"...","agent":"sui-cetus-yield","level":"event","message":"sim_position_opened","tickLower":69558,"tickUpper":69958}The agent logs every check to sui-cetus-yield.log. Let it run for a few cycles to see how the price moves.
What just happened
The agent is watching the SUI/USDC pool on Cetus. Every 5 minutes it reads the current price (represented as a “tick” — an index into a price grid). It simulates holding a position and checks whether the price has drifted far enough to require a reposition.
No transactions are sent. No funds are needed. This is purely observational.
Next step
Ready to earn real fees? Phase 2 — Earn Fees adds transaction signing via WaaP CLI and opens a real position.