SATURN Bots & AI agents Interactive docs · Full reference · llms-full.txt · Launch v4 ↗

Bots & AI agents on Saturn

Saturn v4 is built to be used by programs as much as by people. Every contract is permissionless: a trading bot or an AI agent calls the same methods a wallet does, pays the same fees, and is held to the same on-chain rules. This guide shows the ways a program can make money on Saturn, where it can borrow capital for a trade, and exactly which methods to call.

Everything below runs on Phantasma mainnet (RPC https://pharpc1.phantasma.info/rpc, nexus mainnet) and devnet (https://devnet.phantasma.info/rpc, nexus testnet). A bot needs a Phantasma wallet with KCAL for gas and a little SOUL for new storage keys. Reads are free; writes are signed by the bot's own wallet.

Every trade that borrows capital is atomic: the money has to be back before the transaction ends, or the whole transaction is undone and only the gas is spent.

Reading the market: pools, prices, quotes and the data API#

Every price on Saturn v4 comes from pool reserves that anyone can read for free: saturnrouter and saturnpools views list the pools, return reserves and fees, pick the best pool for a trade and give the raw minimum a swap must meet. A bot quotes a swap off-chain with the same integer math as saturnswap, uses time-weighted prices where a pool is tracked, and can take a 15-second snapshot of every pool and token from the public data API (apiops.saturnx.cc) without any RPC calls.

Contractssaturnrouter-4.1.1, saturnswap-4.4.3, saturnpools-4.1.10 (same on mainnet and devnet)
Cost to readFree: view calls through invokeRawScript, no signature and no gas
Swap costThe pool fee, 30-3000 per 10,000 (mainnet: RA/TAZ reference pool 30, SOUL/KCAL pools 200), plus about 0.05 KCAL gas per swap (mainnet: 0.047-0.054 KCAL for one swap, 0.08-0.10 KCAL for two in one transaction)
Who earns the swap feeProvider 10%, protocol admin 20%, stakers of the input token 10% (only when it has stakers); the rest stays in the pool (saturnrouter.getFeeSplitRatios)
Minimum swap0.01 token today: 1,000,000 raw SOUL, 10,000,000 raw RA or TAZ, 100,000,000 raw KCAL
UnitsAmounts are raw; v4 reserves are 8-decimal scaled; getPoolPrice is × 1e8; TWAP is × 10^18 × seconds; fees are per 10,000
Best-pool scanUp to 100 pools of one pair per getBestPoolForSwapV2 call
Data APIhttps://apiops.saturnx.cc: read-only JSON for mainnet and devnet, refreshed about every 15 s, 300 requests/min per IP (1,200 with an API key)
Mainnet market20 active v4 pools out of 35 created (2026-09-28)

How

  1. Snapshot everything off-chain: GET https://apiops.saturnx.cc/v1/pools?network=mainnet (every v4 and v2 pool with reserves, feeBps, active, burned, lock counts and tvlUsd) and GET https://apiops.saturnx.cc/v1/tokens?network=mainnet (price, liquidity, 24h change). The payload is under data. Divide v4 reserveAScaled / reserveBScaled by 10^8 and v2 pools (labelled V3 in the UI) by 10^max(8, decimals). Pool ids look like v4:<poolId> or v2:<pairKey>. The API refreshes about every 15 s; check GET /v1/health (indexer.stale) before you trust it. The rules are at https://apiops.saturnx.cc/llms.txt and the schema at /openapi.json.
  2. List pools on-chain in one call: saturnpools.getActivePoolsData() yields one "poolId|tokenA|tokenB|reserveA|reserveB|feePer10k|active" row per live pool, and saturnpools.getAllPoolsData() adds removed ones. For one pair, loop saturnrouter.getPoolIdForPairAtIndex(tokenA, tokenB, i) for every i below saturnrouter.getPoolCountForPair(tokenA, tokenB). Removed pools keep their index, so skip any with active = 0.
  3. Right before a trade, read the pool fresh with saturnrouter.getPoolFullInfo(poolId). It returns tokenA, tokenB, resA and resB (8-decimal scaled), fee (per 10,000), active, campLocks and finLocks. saturnrouter.getPoolPrice(poolId, token) gives the spot price of token in the other token × 1e8.
  4. Pick the pool: saturnrouter.getBestPoolForSwapV2(tokenIn, tokenOut, amountIn, maxPools) scores the first maxPools (1-100) pools registered for the pair with the engine's math. Removed pools use up slots, so pass saturnrouter.getPoolCountForPair (at least 1, at most 100). It returns the best poolId, or 0 when no scanned pool gives any output. A non-zero id does not mean the swap passes the minimum and rounding checks below. It does not return the output amount.
  5. Quote to the raw unit off-chain, exactly as saturnswap does: sIn = saturnpools.scaleUp(amountIn, tokenIn); afterFee = sIn − sIn × fee / 10000; sOut = afterFee × rOut / (rIn + afterFee), all with integer division; amountOut = saturnpools.scaleDown(sOut, tokenOut). The conversion is scaled = raw × saturnpools.getScaleFactor / saturnpools.getScaleDivisor, and a divisor of 0 counts as 1 (devnet pool 84's AMIPOLAKAO has one): SOUL is unchanged, RA and TAZ are divided by 10, KCAL by 100.
  6. Check the limits before signing: amountIn must be at least saturnrouter.getMinRawForSwap(tokenIn) (0.01 token today), the admin slice (20% of the pool fee on amountIn) must be at least 1 raw unit, and amountOut must be above 0.
  7. Swap: sign saturnswap.swap(from, poolId, amountIn, tokenIn, tokenOut, minAmountOut), with minAmountOut set to your quote minus your slippage tolerance. It returns the raw amount received and emits SwapExecuted.
  8. For a price one transaction cannot move, use a tracked pool's TWAP. When saturnpools.getTwapTracked(poolId) = 1, read saturnpools.getTwapCumulativeA(poolId) twice for the price of tokenA in tokenB (getTwapCumulativeB gives tokenB in tokenA). (c2 − c1) / (t2 − t1) / 10^18 is the average price, valid as long as saturnpools.getTwapSince(poolId) stays before your first reading. Mainnet tracks the RA/TAZ reference pool 33.
  9. Screen pools before you rely on them. saturnpools.getPoolWithdrawable(poolId) = 0 means the liquidity is burned or time-locked and cannot be pulled. saturnpools.getPoolPawned(poolId) = 1 means the pool backs a loan. The API's burned flag covers burning only. It misses time locks (mainnet pools 12 and 31 read burned false but getPoolWithdrawable 0) and loan pledges (pool 35), so check both views on-chain.

Know before you build

Methods

saturnrouter.getBestPoolForSwapV2() saturnrouter.getPoolFullInfo() saturnrouter.getPoolPrice() saturnrouter.getPoolCountForPair() saturnrouter.getPoolIdForPairAtIndex() saturnrouter.getMinRawForSwap() saturnrouter.getFeeSplitRatios() saturnpools.getActivePoolsData() saturnpools.getAllPoolsData() saturnpools.scaleUp() saturnpools.scaleDown() saturnpools.getScaleFactor() saturnpools.getScaleDivisor() saturnpools.computeAndStoreScaleFactor() saturnpools.getTwapTracked() saturnpools.getTwapCumulativeA() saturnpools.getTwapCumulativeB() saturnpools.getTwapSince() saturnpools.getPoolWithdrawable() saturnpools.getPoolPawned() saturnswap.swap()

Try it

Saturn Ops ↗

Arbitrage with borrowed liquidity (saturnflash) or your own (saturnarb)#

saturnflash lets a bot arbitrage two Saturn v4 pools of the same pair with no capital: it borrows the input from saturnliquidity (every pool's reserves), runs both swaps, returns the principal, pays a 0.05% flash fee to the protocol and hands the bot the rest, in one transaction that reverts if there is no profit. saturnarb runs the same two-pool route with the bot's own tokens, charges no flash fee and returns 100% of the profit. Either way a failed attempt costs only gas, so the work is finding and sizing the price gap off-chain.

Contractssaturnflash-4.2.4 (borrowed liquidity) and saturnarb-4.4.0 (own capital), same versions on mainnet and devnet
Capital neededsaturnflash: none. saturnarb: amountIn of tokenStart in your wallet
Where the loan comes fromsaturnliquidity, the custody balance holding every v4 pool's reserves; ceiling = saturnflash.getMaxBorrowable(token)
Flash fee5 per 10,000 of amountIn (0.05%), paid to the protocol admin wallet; admin can set 1-100 (getFlashFeePer10k). saturnarb has no flash fee
Who earnsThe executor keeps the net profit (saturnflash) or 100% of the profit (saturnarb). Each leg pays its pool's swap fee: provider 10%, admin 20%, holders 10% (only when the input token has stakers), the rest reinvested into the pool
Break-even gapabout feeBuy + feeSell (+ 5 per 10,000 for saturnflash) plus price impact. Pool fees range 30-3000 per 10,000; many mainnet pools charge 200 (2%)
Routeexactly two different active saturnpools (v4) pools holding the same pair: tokenStart → tokenMid → tokenStart
Minimum sizeeach leg at least saturnrouter.getMinRawForSwap (0.01 token today: 1,000,000 raw SOUL, 100,000,000 raw KCAL); the flash fee must be at least 1 raw unit
Gasabout 0.11-0.15 KCAL per arbitrage call (measured on devnet), paid even when it reverts
If it failsthe whole transaction reverts: nothing borrowed, no flash fee, no swap fees, only gas spent
Pause switchsaturnflash.getFlashEnabled() (1 = on). saturnarb has none

How

  1. Find pairs that have two or more active v4 pools: saturnpools.getActivePoolsData() returns one "poolId|tokenA|tokenB|reserveA|reserveB|feePer10k|active" row per active pool. For one pair use saturnrouter.getPoolCountForPair(tokenA, tokenB) and saturnrouter.getPoolIdForPairAtIndex(tokenA, tokenB, i); that index also keeps removed pools, so keep only pools where saturnpools.getPoolActive is 1. GET https://apiops.saturnx.cc/v1/pools?network=mainnet works without RPC, but it also lists v2 and inactive pools: keep rows with version v4 and active true, use numericId as the poolId, and expect it to lag the chain by about 15 seconds.
  2. Right before trading, read fresh reserves and fees on-chain with saturnrouter.getPoolFullInfo(poolId) (tokenA, tokenB, resA, resB, fee, active) or saturnpools.getPoolReserveA / getPoolReserveB / getPoolFee. Reserves are 8-decimal scaled units while the amounts you send are raw: convert with the free views saturnpools.scaleUp(amount, symbol) and saturnpools.scaleDown(amount, symbol) (scaled = raw × getScaleFactor / getScaleDivisor). Today KCAL (10 decimals) is raw / 100, TAZ (9) raw / 10, SOUL (8) unchanged.
  3. Compare spot prices with saturnrouter.getPoolPrice(poolId, tokenStart), which returns tokenMid per tokenStart × 1e8. poolIdBuy is the pool with the highest price (tokenStart buys the most tokenMid), poolIdSell the one with the lowest. A gap is worth simulating only when priceBuy / priceSell − 1 is larger than feeBuy + feeSell (per 10,000) plus the 5 per 10,000 flash fee.
  4. Simulate both legs with the swap engine's own math, per leg in scaled units, rounding down: inAfterFee = in − in × feePer10k / 10000; out = inAfterFee × reserveOut / (reserveIn + inAfterFee). finalAmount = leg2(leg1(amountIn)). For saturnflash, net = finalAmount − amountIn − saturnflash.quoteFlashFee(amountIn); for saturnarb, profit = finalAmount − amountIn. Search amountIn for the maximum; your own trade closes the gap, so larger is not better.
  5. Pre-flight with free reads: saturnflash.getFlashEnabled() must be 1; amountIn must be at most saturnflash.getMaxBorrowable(tokenStart); amountIn must be at least saturnrouter.getMinRawForSwap(tokenStart) and the leg 1 output at least saturnrouter.getMinRawForSwap(tokenMid); saturnflash.quoteFlashFee(amountIn) must be above 0. You cannot dry-run the write with invokeRawScript (the witness check fails), so your simulation is the quote.
  6. No capital: sign and send saturnflash.executeFlashArb(from, poolIdBuy, poolIdSell, tokenStart, amountIn, minNetProfit), with minNetProfit set to the smallest net profit in raw tokenStart you accept (for example your gas converted to tokenStart plus a margin). It returns netProfit and emits FlashArbExecuted; the principal goes back to saturnliquidity and the fee to the protocol admin wallet.
  7. Own capital: hold amountIn of tokenStart in the signing wallet and send saturnarb.executeArbitrage(from, poolIdBuy, poolIdSell, tokenStart, amountIn, minProfit). There is no flash fee, and finalAmount (your capital plus 100% of the profit) comes back to your wallet. The method returns nothing, so read the ArbExecuted event or your balance change.
  8. After each trade, re-read both pools (your arb moved them) and track results with saturnflash.getExecutorTotalProfit(from) or saturnarb.getExecutorTotalProfit(from). Those add raw amounts of different tokens together, so keep your own per-token ledger from the events.

Know before you build

Methods

saturnflash.executeFlashArb() saturnflash.quoteFlashFee() saturnflash.getMaxBorrowable() saturnflash.getFlashFeePer10k() saturnflash.getFlashEnabled() saturnflash.getExecutorTotalProfit() saturnarb.executeArbitrage() saturnarb.getExecutorTotalProfit() saturnpools.getActivePoolsData() saturnrouter.getPoolCountForPair() saturnrouter.getPoolIdForPairAtIndex() saturnrouter.getPoolFullInfo() saturnrouter.getPoolPrice() saturnrouter.getMinRawForSwap() saturnpools.scaleUp() saturnpools.scaleDown() saturnliquidity.getLiquidityAddress()

Try it

Saturn Ops ↗

Arbitrage with borrowed staked capital (saturnstakearb)#

saturnstakearb lets a bot borrow the idle tokens that holders have staked in saturnholders, arbitrage two Saturn v4 pools of the same pair with them, and return every token in the same transaction. There is no fee and no capital needed: the profit is split, half (rounded down) to the bot and the rest to that token's stakers, and a round trip that does not come back with more than it borrowed reverts, costing only gas. On thin gaps it leaves the bot more than saturnflash, and it pays stakers a yield on top of swap fees without risking their stake, so it is also an easy pitch to a token's holders.

Contractssaturnstakearb-4.4.0 and saturnholders-4.4.2, same versions on mainnet and devnet
Capital needednone; the bot only pays gas in KCAL
Where the loan comes fromthe tokens staked in saturnholders for tokenSymbol; ceiling = saturnholders.getTotalStaked(tokenSymbol), pledge-locked stake included
Feeno flash or protocol fee. Each leg pays its pool's swap fee: provider 10%, admin 20%, holders 10% (only when the input token has stakers), the rest reinvested into the pool
Who earnsprofit = back − amountIn after both swap fees. Bot: floor(profit / 2). Stakers of tokenSymbol: the rest, plus the holder slice of leg 1's swap fee (leg 1 sells tokenSymbol)
vs saturnflashflash leaves profit − 0.05% of amountIn, stake-arb leaves profit / 2: stake-arb pays the bot more when the profit is under about 0.1% of amountIn, flash above that
Routeexactly two different active saturnpools (v4) pools of the pair tokenSymbol/riskToken: tokenSymbol → riskToken → tokenSymbol
Minimum sizeeach leg at least saturnrouter.getMinRawForSwap (0.01 token today: 1,000,000 raw SOUL, 100,000,000 raw KCAL); a profit of 1 raw unit pays the bot 0
Gasabout 0.11–0.15 KCAL per executeArb (measured on devnet), paid even when it reverts
Stakers' principalsettleArbLoan requires saturnholders to hold at least the total staked before the transaction commits; the loan exists only inside that transaction (getLoanOpen reads 0 between transactions), so unstaking is never blocked
Pause switchnone; saturnstakearb has no enable flag (the Saturn admin can only upgrade the contract)
Usage so farsaturnstakearb.getTotalArbs() = 0 on mainnet and 11 on devnet (2026-09-28)

How

  1. Pick the token to borrow. saturnholders.getStakedTokensData() returns one "symbol|totalStaked|accFeePerToken|stakers|lifetimeAccrued|loanOpen" row per token ever staked; skip rows with totalStaked 0. The most you can borrow is saturnholders.getTotalStaked(tokenSymbol), read it right before each trade.
  2. Find pairs of that token with two or more active v4 pools: saturnpools.getActivePoolsData(), or saturnrouter.getPoolCountForPair(tokenA, tokenB) and saturnrouter.getPoolIdForPairAtIndex(tokenA, tokenB, i), which also keep removed pools (use only pools where saturnpools.getPoolActive is 1; executeArb reverts on an inactive pool). The other token of the pair is riskToken. When both tokens of a pair are staked, either side can take the same gap: borrowing the other token just swaps poolBuy and poolSell. Simulate both and take the larger botShare.
  3. Read fresh reserves and fees on-chain with saturnrouter.getPoolFullInfo(poolId) and compare saturnrouter.getPoolPrice(poolId, tokenSymbol). poolBuy is the pool where tokenSymbol buys the most riskToken, poolSell the one where it buys the least (riskToken sells dearest there).
  4. Simulate both legs with the swap engine's math, in 8-decimal scaled units and rounding down (convert with saturnpools.scaleUp / scaleDown): inAfterFee = in − in × feePer10k / 10000; out = inAfterFee × reserveOut / (reserveIn + inAfterFee). profit = back − amountIn and your share is floor(profit / 2). Search amountIn for the best botShare, within amountIn ≤ getTotalStaked(tokenSymbol), amountIn ≥ saturnrouter.getMinRawForSwap(tokenSymbol) and leg 1 output ≥ saturnrouter.getMinRawForSwap(riskToken).
  5. Send only if botShare, converted to KCAL, is clearly above your gas (about 0.11–0.15 KCAL). Also compute the saturnflash alternative (profit − 0.05% of amountIn, saturnflash.quoteFlashFee) and use whichever leaves you more.
  6. Sign and send saturnstakearb.executeArb(from, tokenSymbol, amountIn, riskToken, poolBuy, poolSell) from the bot's wallet. There is no minProfit argument, so send straight after the read. Your botShare arrives in the signing wallet in tokenSymbol; the call returns holderShare and emits StakeArbExecuted (tokenSymbol, amountIn, backAmount, botShare, holderShare).
  7. Track results with saturnstakearb.getExecutorArbCount(from), saturnstakearb.getTotalArbs() and saturnstakearb.getTotalHolderProfit(tokenSymbol) (lifetime profit paid to stakers), and keep your own per-token ledger from the events.
  8. For holders (the pitch): stake with saturnholders.stake(from, tokenSymbol, amount). Each stake arbitrage raises saturnholders.getPendingRewards(user, tokenSymbol) at once, alongside the swap-fee slice; collect both with saturnholders.claim(from, tokenSymbol) and leave any time with saturnholders.unstake(from, tokenSymbol, amount) (only a saturntaz pledge, saturnholders.getPledgeLocked, can hold part of a stake).

Know before you build

Methods

saturnstakearb.executeArb() saturnstakearb.getTotalArbs() saturnstakearb.getTotalHolderProfit() saturnstakearb.getExecutorArbCount() saturnstakearb.getContractVersion() saturnholders.getStakedTokensData() saturnholders.getTotalStaked() saturnholders.flashLendStake() saturnholders.settleArbLoan() saturnholders.getLoanOpen() saturnholders.accrueHolderFee() saturnholders.stake() saturnholders.unstake() saturnholders.claim() saturnholders.getPendingRewards() saturnholders.getLifetimeAccrued() saturnholders.getPledgeLocked() saturnpools.getActivePoolsData() saturnrouter.getPoolCountForPair() saturnrouter.getPoolIdForPairAtIndex() saturnrouter.getPoolFullInfo() saturnrouter.getPoolPrice() saturnrouter.getMinRawForSwap() saturnflash.quoteFlashFee()

Try it

Saturn Ops ↗

Strategy vaults: trade pooled capital, profit-only#

saturnvaults lets a bot open a public vault in one base token, take deposits from anyone, and arbitrage with that pooled money through agentArb (two pools) or agentArb3 (three pools). Every trade has to end with more base token than it started, measured on the contract's own balance, or it reverts, so depositors can only gain in base-token terms. The bot earns its fixed fee (1–30%) of each trade's profit, paid out at once. Use it when you have an arbitrage strategy and want more capital than your own, with no flash-loan fee and a public on-chain track record (share price, trade log) that brings in depositors.

Versionsaturnvaults-4.2.0 (check getContractVersion before trading)
CapitalDepositors' base token. One trade may use up to the vault's own NAV (getVaultTotalDeposits). There is no borrowing fee (saturnflash charges 5 per 10,000 of the borrowed amount).
Agent fee1–30% of each trade's gross profit (perfFeePer10k 100–3000). Fixed at creation and paid in the base token at every trade.
Depositors earnThe rest of every profit, added to the vault's NAV. The share price (8 decimals, 100000000 = 1.0) starts at 1.0 and never falls.
Profit ruleThe trade must end with more base token than it started (the contract's balance) and at least minProfit more, and no intermediate token balance may end lower. Otherwise the whole transaction reverts.
Swap feesEach leg pays the pool's normal fee: provider 10%, admin 20%, holders 10% (only when the input token has stakers), and the rest is reinvested into the pool.
Gas (devnet, measured)createVaultV2 ~0.021 KCAL, deposit / withdrawV2 ~0.024, closeVault ~0.017, agentArb ~0.11, agentArb3 ~0.15 KCAL. A trade refused at the profit check costs ~0.044 KCAL (2 pools) or ~0.058 KCAL (3 pools).
Who can tradeOnly the vault's creator (its agent). The agent cannot be changed.
Hold time0–30 days, set at creation and only ever lowered (a lower hold applies once the current one has run). Every top-up restarts it for the whole position. A closed vault has no hold.
Vault name1–40 printable ASCII characters
Track recordOn-chain trade log (getVaultTrades) and the 22-field getVaultStats. getVaultProfitToOthers is the profit earned for depositors other than the agent.
Usage so farNo vaults on mainnet yet: getNextVaultId() = 1 and getTotalArbTrades() = 0, so there are no depositors to trade for. Devnet: 32 vaults and 4 trades (2026-09-28).

How

  1. Check the deployment: saturnvaults.getContractVersion must return "saturnvaults-4.2.0". On 4.1.3, agentArb, agentArb3, createVaultV2 and the market views do not exist.
  2. Open the vault from the bot's own key with saturnvaults.createVaultV2(from, baseToken, perfFeePer10k, minDeposit, name, minHoldSeconds): a fee of 100–3000 per 10,000, a printable-ASCII name of 1–40 characters, and a hold of 0–2592000 s. The method returns nothing, so read the new vaultId from the VaultCreated event. This key is the vault's agent for good.
  3. Optionally seed the vault yourself with saturnvaults.deposit(from, vaultId, amount). Others deposit the same way, and each deposit restarts that depositor's hold for their whole position.
  4. Find price gaps. List the pools for a pair with saturnrouter.getPoolCountForPair and saturnrouter.getPoolIdForPairAtIndex, and read reserves and fees with saturnrouter.getPoolFullInfo (or GET https://apiops.saturnx.cc/v1/pools?network=mainnet, v4 rows only: numericId is the poolId, and v2 rows cannot be traded here). Both give reserves in 8-decimal scaled units, not raw: convert with saturnpools.scaleUp / scaleDown (KCAL raw / 100, TAZ and RA raw / 10, SOUL unchanged). Skip inactive pools (active:0 in getPoolFullInfo, active false in the API): the pair lists keep them (mainnet SOUL/KCAL pool 10 is one), and a leg there reverts with "Pool not active". Then simulate every leg (constant product minus each pool's swap fee) to find the baseIn that makes the most profit.
  5. Size the trade: baseIn must be <= saturnvaults.getVaultTotalDeposits(vaultId), and each leg's input must be at least saturnrouter.getMinRawForSwap(tokenIn).
  6. Send saturnvaults.agentArb(from, vaultId, baseIn, riskToken, poolBuy, poolSell, minProfit) or saturnvaults.agentArb3(from, vaultId, baseIn, tokenX, tokenY, pool1, pool2, pool3, minProfit). Set minProfit to your simulated gross profit minus a margin, so a gap that has already been closed makes the trade revert instead of filling thin.
  7. Track results with saturnvaults.getVaultStats (22 fields, name last), saturnvaults.getVaultTrades(vaultId, start, count) and saturnvaults.getVaultSharePrice (8 decimals). Depositors check saturnvaults.getUserValue and saturnvaults.getUserUnlockAt, then exit with saturnvaults.withdrawV2.
  8. Manage the terms with saturnvaults.setVaultName and saturnvaults.setVaultMinHold (lower only; the new hold applies once the current one has run). Retire the vault with saturnvaults.closeVault, which frees every depositor to withdraw at once.

Know before you build

Methods

saturnvaults.getContractVersion() saturnvaults.createVaultV2() saturnvaults.deposit() saturnvaults.getVaultTotalDeposits() saturnvaults.agentArb() saturnvaults.agentArb3() saturnvaults.getVaultStats() saturnvaults.getAllVaultsStats() saturnvaults.getVaultTrades() saturnvaults.getVaultSharePrice() saturnvaults.getVaultProfitToOthers() saturnvaults.getUserValue() saturnvaults.getUserUnlockAt() saturnvaults.withdrawV2() saturnvaults.setVaultName() saturnvaults.setVaultMinHold() saturnvaults.closeVault() saturnrouter.getPoolCountForPair() saturnrouter.getPoolIdForPairAtIndex() saturnrouter.getPoolFullInfo() saturnrouter.getMinRawForSwap()

Try it

Saturn Ops ↗

Borrow TAZ for days or months against an RA/TAZ pool (saturnmarket)#

saturnmarket is a peer-to-peer market for TAZ loans of 7 to 365 days. The borrower pledges an RA/TAZ Saturn v4 pool they provide; the pool keeps trading and keeps earning the borrower's provider fees. Lenders post binding quotes with the TAZ escrowed, and the borrower accepts one and receives the TAZ in the same transaction. It suits a bot that needs working capital or TAZ inventory for longer than one transaction. It is not a flash loan, and it needs a lender to quote.

Contractssaturnmarket (no version getter), saturnloans-1.0.3, saturnvault-1.1.0, saturndexadapt-1.3.0, saturntaz-1.2.3, saturnlendcfg; the same on mainnet and devnet.
Loan tokenTAZ only (9 decimals). Collateral: one RA/TAZ v4 pool the borrower provides.
InterestSimple annual rate per 10,000 over the quoted term: principal × rate × duration in seconds / (10,000 × 31,536,000). It is fixed at acceptance and owed in full: repaying early does not lower it. No on-chain cap on quotes.
FeesAn origination fee of 1% of principal goes to the protocol admin at acceptance, and 10% of the interest portion of each payment goes to the protocol admin. The rest goes to the lender, escrowed in saturnloans.
Term7-365 days, installments every 30 days, grace period of 3 days after the due date.
LiquidationLender only. It needs LTV above 90% at the reference RA/TAZ pool's TWAP, with a flag first and the trigger 6-24 h later on mainnet (120-1,200 s on devnet).
Limits5 active loans per borrower; requests stay open 1-30 days.
TAZ reward30% borrower / 30% lender / 40% pledgers, capped by the protocol fees the loan paid. It is 0 on mainnet today and paid on devnet.
GasAbout 0.02-0.15 KCAL per call.

How

  1. Get collateral: provide an RA/TAZ pool (saturnliquidity.createPool) and keep its SATURN certificate. saturndexadapt.v4PoolPledgeable(you, poolId) must be 1 (no bond, rental, option, campaign, burn, time lock or fee redirect). The pool's value is saturndexadapt.v4PoolValueInBase(poolId, "TAZ", 2) = 2 × √(k × P), with P taken from the reference pool (saturndexadapt.getReferencePool). The value is in 8-decimal scaled TAZ and loan amounts are raw 9-decimal TAZ, so multiply the value by 10 before you compare them.
  2. Read the terms from saturnlendcfg: getMinLoanDuration / getMaxLoanDuration (7-365 days), getOriginationFeeBps (100 = 1%), getProtocolFeeShare (1,000 = 10% of interest), getGracePeriod (3 days), getLiquidationThreshold (9,000 = 90% LTV) and getMaxLoansPerUser (5). saturnmarket.getMarketEnabled() must be 1.
  3. Borrower posts: saturnmarket.postLoanRequest(from, "TAZ", 2, loanAmountRaw, 2, "", 0, 0, poolId, 0, preferredDuration, maxInterestRate, message, expiresInSeconds). Rates are annual per 10,000, and a request stays open 1-30 days.
  4. Lender bot scans requests 1 to saturnmarket.getNextRequestId() − 1 with saturnmarket.getRequestSummary and saturnmarket.getRequestExpiresAt (status stays 1 after expiry). It reads the borrower with saturnmarket.getRequestBorrower and checks saturnmarket.getBorrowerProfile(borrower), whose score is a cached value (simulate saturncredit.computeScore(borrower) for the live one), and gets the collateral from saturnmarket.getRequestCollateralPoolId, valued with saturndexadapt.v4PoolValueInBase.
  5. Lender quotes: saturnmarket.submitQuote(from, requestId, interestRate, duration, offeredLoanAmount, 2, "", 0, 0, requestPoolId, 0, message, expiresInSeconds). The TAZ is escrowed at once; saturnmarket.withdrawQuote(from, quoteId) returns it at any time before acceptance. acceptQuote checks no LTV, so the borrower can still accept after the pool has lost value, until the quote expires: keep expiresInSeconds short, or withdraw a quote whose pool fell.
  6. Borrower accepts: list quotes with saturnmarket.getRequestQuoteCount / getRequestQuoteAtIndex / getQuoteSummary, then call saturnmarket.acceptQuote(from, quoteId). The pool is pledged, the certificate moves to saturnvault, and the quote's amount − 1% arrives in the same transaction. You owe the full quote amount plus interest for the whole quoted term. loanId = saturnmarket.getRequestLoanId(requestId).
  7. Track the loan with saturnloans.getLoanRemaining, getLoanDueDate, getLoanNextInstallmentDue / installmentOverdue (30-day installments affect only the credit score) and getCurrentLtv. Once the loan is flagged, also read getLoanLiquidationFlaggedAt and getLiquidationTwapLtv.
  8. Repay with saturnloans.makePayment(from, loanId, rawTaz), as many times as you like; the final payment returns the certificate. getLoanRemaining is in 8-decimal scaled TAZ, so pay getLoanRemaining × 10 raw TAZ to clear the loan. A larger amount is cut to what is owed, but the wallet must hold all of it. Then anyone can call saturnloans.claimRepaymentReward(loanId) within one reward day (86,400 s on mainnet, 60 s on devnet; saturnloans.getLoanRewardClaimDeadline) for the TAZ reward (saturntaz.previewRewardForLoan shows the amount).
  9. Lender: collect repayments with saturnloans.getLenderBalance / withdrawLenderBalance(from, "TAZ"). If the borrower is still unpaid past the due date + 3 days, call saturnloans.triggerDefault(loanId). If LTV is above 90%, call saturnloans.flagLiquidation(loanId), then saturnloans.triggerLiquidation(loanId) 6-24 h later. Either way the lender becomes the pool's provider and receives the certificate.
  10. RA holders who stake RA in saturnholders can back a lender with saturntaz.pledgeV4(from, lender, amount) for 30 days (300 s on devnet), then collect with saturntaz.claimPledgeRewards. Pledger rewards are 0 on mainnet today: a mainnet pledge locks that staked RA for 30 days and earns no TAZ.

Know before you build

Methods

saturnmarket.postLoanRequest() saturnmarket.submitQuote() saturnmarket.acceptQuote() saturnmarket.withdrawQuote() saturnmarket.cancelRequest() saturnmarket.getNextRequestId() saturnmarket.getRequestSummary() saturnmarket.getRequestExpiresAt() saturnmarket.getRequestCollateralPoolId() saturnmarket.getRequestQuoteCount() saturnmarket.getRequestQuoteAtIndex() saturnmarket.getQuoteSummary() saturnmarket.getRequestLoanId() saturnmarket.getBorrowerProfile() saturnmarket.getMarketEnabled() saturndexadapt.v4PoolPledgeable() saturndexadapt.getReferencePool() saturndexadapt.v4PoolValueInBase() saturnloans.makePayment() saturnloans.getLoanRemaining() saturnloans.getLoanDueDate() saturnloans.installmentOverdue() saturnloans.getCurrentLtv() saturnloans.getLoanLiquidationFlaggedAt() saturnloans.getLiquidationTwapLtv() saturnloans.flagLiquidation() saturnloans.triggerLiquidation() saturnloans.triggerDefault() saturnloans.getLenderBalance() saturnloans.withdrawLenderBalance() saturnloans.claimRepaymentReward() saturntaz.previewRewardForLoan() saturntaz.getRewardParam() saturntaz.pledgeV4() saturntaz.claimPledgeRewards() saturnlendcfg.getLiquidationThreshold() saturnlendcfg.getMinLoanDuration() saturnlendcfg.getMaxLoanDuration() saturnlendcfg.getOriginationFeeBps() saturnlendcfg.getProtocolFeeShare() saturnlendcfg.getGracePeriod() saturnlendcfg.getMaxLoansPerUser() saturnliquidity.createPool() saturnholders.stake()

Try it

Saturn Ops ↗

Earn keeper bounties: limit orders (saturnlimit) and TWAMM chunks (saturntwamm)#

Saturn users can leave work on chain for bots (none on mainnet yet, 2026-09-28): limit orders waiting for a price, and TWAMM streams that swap a large amount in chunks over time. Anyone can execute a limit order once its pool pays the owner's minimum, and keeps up to 5% of whatever the fill pays above that minimum. Anyone can fire a stream's next chunk once its pacing time has passed, and keeps up to 5% of that chunk's output. You need no capital, only KCAL for gas and a little SOUL. The work is finding orders and chunks that are due and sending only the ones that will go through. Before you run a bot, check saturnlimit.getActiveOrderCount and saturntwamm.getActiveStreamCount: both are 0 on mainnet today.

Contractssaturnlimit-4.1.4 and saturntwamm-4.2.4, same on mainnet and devnet
Capital neededNone. KCAL for gas and a little SOUL for the transaction data fee
Limit-order bountybountyPer10k (0-500, max 5%) of the surplus: (amountOut − minAmountOut) × bountyPer10k / 10000, paid in tokenOut out of the order's output. It is zero when the fill is exactly at the limit, and the owner always gets at least minAmountOut
TWAMM bountybountyPer10k (0-500, max 5%) of each chunk's output after the pool fee, paid in tokenOut out of the stream owner's output. The chunk floor is checked before the bounty is taken
Who paysThe order or stream owner, out of their output. Each execution also pays the pool's normal swap fee (provider 10%, admin 20%, holders 10% when the input token has stakers, the rest reinvested into the pool)
When a limit order is executableStatus 0, expiry 0 or now < expiry, pool quote >= minAmountOut and <= maxAmountOut (if set), amountIn >= swap minimum
When a chunk is executablenow >= lastExecutionTime + minChunkSeconds (at least 60 s), chunk >= swap minimum, pool quote >= the chunk's floor
GasAbout 0.065 KCAL per TWAMM chunk (measured on devnet). executeOrder runs the same single-swap path. Gas is paid even when the call reverts
Break-evenBounty value >= gas: at 50 (0.5%) the limit surplus or chunk output must be worth about 13 KCAL; at 500 (5%) about 1.3 KCAL
Swap minimumsaturnrouter.getMinRawForSwap(token): 0.01 token today (1,000,000 raw SOUL, 100,000,000 raw KCAL)
Saturn's TWAMM keeperOff on mainnet and devnet as of 2026-09-28 (GET /v1/twamm/keeper). When enabled: every 15 s, bounty >= 1 per 10,000, up to 5 chunks per tick. There is no Saturn keeper for limit orders
ActivityMainnet has had no limit orders or streams yet (getNextOrderId = 1, getNextStreamId = 1). Devnet: 25 orders (6 executed), 36 streams (11 completed), as of 2026-09-28
Human UILimit orders: https://ops.saturnx.cc/trade (Limit mode). Streams: https://ops.saturnx.cc/twamm (has an Execute chunk button)

How

  1. Check versions first, because the rules below depend on them: saturnlimit.getContractVersion() must return "saturnlimit-4.1.4" and saturntwamm.getContractVersion() must return "saturntwamm-4.2.4".
  2. Limit orders, read the book: saturnlimit.getActiveOrdersData() returns one "orderId|poolId|owner|tokenIn|tokenOut|amountIn|minAmountOut|maxAmountOut|bountyPer10k|expiry|status" row per live order. Executed, cancelled and expired orders are already removed. A bot watching one market can use saturnlimit.getActiveOrderIdsByPool(poolId) instead.
  3. Filter the rows: skip bountyPer10k 0, skip rows where expiry > 0 and now >= expiry minus a few seconds (executeOrder needs now < expiry), and skip amountIn below saturnrouter.getMinRawForSwap(tokenIn). 4.1.4 refuses such orders at placement, but older orders and a raised minimum can still leave some that can never execute.
  4. Quote each order from fresh on-chain reserves: saturnrouter.getPoolFullInfo(poolId), or saturnpools.getPoolReserveA / getPoolReserveB / getPoolFee. Reserves are 8-decimal scaled. Compute in = saturnpools.scaleUp(amountIn, tokenIn), inAfterFee = in − in × feePer10k / 10000, outScaled = inAfterFee × reserveOut / (reserveIn + inAfterFee), out = saturnpools.scaleDown(outScaled, tokenOut), rounding down at each step. The order is executable when out >= minAmountOut and either maxAmountOut = 0 or out <= maxAmountOut.
  5. Work out what you earn: bounty = (out − minAmountOut) × bountyPer10k / 10000 raw tokenOut. Value it, for example with GET https://apiops.saturnx.cc/v1/prices?network=mainnet. If it beats your gas, sign and send saturnlimit.executeOrder(from, orderId). The OrderExecuted event reports amountOut, ownerPayout and bounty.
  6. Optional cleanup: an order past its expiry stays listed until someone calls saturnlimit.expireOrder(from, orderId). That returns the deposit to its owner, pays the caller nothing and costs gas.
  7. TWAMM streams, list them: saturntwamm.getAllActiveStreamIds(), or saturntwamm.getActiveStreamsData() for pool, tokens and amounts. The rows carry endTime but no other timing or floor data, so for each id read, in one invokeRawScript with several calls: getStreamPoolId, getStreamTokenIn, getStreamTokenOut, getStreamAmountInTotal, getStreamAmountStreamedSoFar, getStreamAmountInRemaining, getStreamStartTime, getStreamEndTime, getStreamLastExecutionTime, getStreamMinChunkSeconds, getStreamMinOutputPerChunk, getStreamBountyPer10k and getStreamFloorMode.
  8. Check whether a chunk is due: now >= lastExecutionTime + minChunkSeconds, by block time, so add about 3 seconds of margin. The first chunk is due minChunkSeconds after placement. chunkIn = remaining if now >= endTime; otherwise chunkIn = floor(total × (now − startTime) / (endTime − startTime)) − streamedSoFar, and 0 means nothing is owed yet. With minRaw = saturnrouter.getMinRawForSwap(tokenIn): if 0 < remaining − chunkIn < minRaw, then chunkIn = remaining (the dust sweep). chunkIn must be at least minRaw.
  9. Compute the floor the chunk must clear. floorMode 1: max(1, minOutputPerChunk × chunkIn × (endTime − startTime) / (total × minChunkSeconds)). floorMode 0: minOutputPerChunk. Quote chunkIn on the stream's pool with the swap math from step 4; the chunk goes through only if chunkOut >= floor.
  10. Work out what you earn: bounty = chunkOut × bountyPer10k / 10000 raw tokenOut. If it beats your gas, send saturntwamm.executeStreamingChunk(from, streamId), one per transaction. Read StreamChunkExecuted for chunkIn, chunkOut and bounty. The chunk that empties a stream also pays the owner (StreamClaimed), so nothing is left to watch afterwards.
  11. Check your competition before and during a run: GET https://apiops.saturnx.cc/v1/twamm/keeper?network=mainnet (or devnet) shows whether Saturn's own TWAMM keeper is enabled, how often it ticks and what it fired recently. When it is on, it fires due chunks with a bounty of at least minBountyBps, about every 15 s, most overdue first, up to 5 per tick. To win a chunk you must land sooner after the pacing window opens. Saturn runs no keeper for limit orders.

Know before you build

Methods

saturnlimit.getContractVersion() saturnlimit.getActiveOrdersData() saturnlimit.getActiveOrderIdsByPool() saturnlimit.getActiveOrderCount() saturnlimit.getOrderInfo() saturnlimit.getNextOrderId() saturnlimit.executeOrder() saturnlimit.expireOrder() saturnlimit.placeOrderV2() saturnlimit.cancelOrder() saturntwamm.getContractVersion() saturntwamm.getAllActiveStreamIds() saturntwamm.getActiveStreamsData() saturntwamm.getStreamPoolId() saturntwamm.getStreamTokenIn() saturntwamm.getStreamTokenOut() saturntwamm.getStreamAmountInTotal() saturntwamm.getStreamAmountStreamedSoFar() saturntwamm.getStreamAmountInRemaining() saturntwamm.getStreamStartTime() saturntwamm.getStreamEndTime() saturntwamm.getStreamLastExecutionTime() saturntwamm.getStreamMinChunkSeconds() saturntwamm.getStreamMinOutputPerChunk() saturntwamm.getStreamBountyPer10k() saturntwamm.getStreamFloorMode() saturntwamm.executeStreamingChunk() saturntwamm.placeStreamingOrder() saturntwamm.cancelStream() saturnrouter.getPoolFullInfo() saturnrouter.getMinRawForSwap() saturnpools.getPoolReserveA() saturnpools.getPoolReserveB() saturnpools.getPoolFee() saturnpools.scaleUp() saturnpools.scaleDown()

Try it

Saturn Ops ↗