Keeps the rioswap (Uniswap v3 fork on Realio Network) tWBTC/tUSDC and tETH/tUSDC pools at the CoinGecko market price.
Every KEEPER_INTERVAL_MINUTES it:
- fetches BTC, ETH and USDC prices from CoinGecko (one request), target =
base_usd / usdc_usd; - reads each pool's
slot0and skips it if it's withinKEEPER_DEVIATION_BPSof the target; - otherwise swaps through the SwapRouter with
exactInputSingle,sqrtPriceLimitX96= the target price andamountIn= min(KEEPER_MAX_SWAP_USDworth, wallet balance).
The price limit does the sizing: the pool stops exactly at the target and the router only pulls the tokens actually used, so there's no liquidity math to get wrong. If the cap runs out first the price moves part-way and the next tick finishes the job.
Pairs live in src/config.ts (PAIRS); chain and contract addresses in
src/chains.ts and deployments/.
pnpm install
cp .env.example .env # fill KEEPER_PRIVATE_KEY, keep KEEPER_DRY_RUN=true at first
pnpm once # one pass, then exit
pnpm start # loop in the foreground (Ctrl-C to stop after the current tick)Run it in the background (settings from .env, logs in logs/):
pnpm keeper:start # no-op if one is already running
pnpm keeper:status # pid, uptime, swaps today, last log lines
pnpm keeper:restart # e.g. after editing .env
pnpm keeper:stop # finishes the current tick (incl. an in-flight swap) firstRestarts are safe: only one keeper can hold logs/keeper.lock (a stale lock from a killed process
is taken over), a new run waits for any tx the previous run left pending, and the daily USD cap is
rebuilt from logs/trades.jsonl instead of resetting.
Set KEEPER_DRY_RUN=false to actually trade. In dry-run it still reports deviation, the direction
and size it would trade, and the simulated output (once the router has an allowance).
The keeper wallet needs RIO for gas plus inventory on both sides: it sells tUSDC when the pool is
under the market and tWBTC/tETH when it's over. On testnet the tokens have faucet() and an open
mint(). Approvals to the router are sent automatically the first time a token is sold.
| Env | Default | |
|---|---|---|
KEEPER_DEVIATION_BPS |
30 | ignore smaller gaps (0.30%) |
KEEPER_MAX_SWAP_USD |
20000 | max value sold per swap |
KEEPER_MAX_DAILY_USD |
500000 | max value sold per UTC day (survives restarts via the trade log) |
KEEPER_MAX_PRICE_AGE_SEC |
600 | skip stale CoinGecko data |
KEEPER_MAX_PRICE_JUMP_PCT |
15 | a bigger tick-to-tick move waits one tick for confirmation |
pnpm test # price math
pnpm typecheckTo try real swaps without touching testnet, fork it with anvil and point the keeper at it:
anvil --fork-url https://json-rpc.realiostage.network --chain-id 3300 --port 18545
# mint test tokens to anvil account 0 with `cast send <token> "mint(address,uint256)" ...`
KEEPER_RPC_URL=http://127.0.0.1:18545 KEEPER_DRY_RUN=false \
KEEPER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
pnpm onceThe key above is anvil's well-known default account 0 — only ever use it against a local fork.
All wallet keys and per-deployment settings live in .env (gitignored). Never commit .env or
logs/ — the logs contain the operating wallet's address and trade history.