Skip to Content
solidondocs

Keeper

What the Solidon testnet keeper does each loop, its flags and environment variables, and how it is deployed.

The keeper (backend/keeper/keeper.js, Node.js 20+) is a convenience process. Every treasury method it calls is open to anyone and checked by the contract; the keeper only decides when to call. It also plays two trusted testnet roles: it feeds the mock oracle, and its NEAR account keeper.solidon.testnet is the factory's bridge and relayer.

One keeper only

Run at most one keeper against a factory. Its state.json keys every bridge transfer by NEAR receipt id; a second keeper with its own state could deliver the same transfer twice. To move a keeper, stop it and move (never copy) its state.json.

The loop

One cycle runs every LOOP_SECS (60 s). Steps run in this order; --only= selects a subset.

Step (--only name)WhenWhat it does
oracleevery ORACLE_INTERVAL_SECS (300 s)Reads latestRoundData() of the 8 Chainlink proxies on Robinhood Chain mainnet and calls set_price on the mock oracle for each, in one NEAR transaction. Skips a feed older than ORACLE_MAX_FEED_AGE_SECS (25 h) or non-positive, so the contracts' 15-minute staleness brake applies. Refuses to write to pyth-oracle.near / pyth-oracle.testnet or any non-.testnet account.
repegevery REPEG_EVERY_MIN minutes (default 0 = off; 30 in production)Moves every synthetic stock pool whose next buy fills more than THRESHOLD_PCT above Chainlink to TARGET x Chainlink, adding DEPTH_USD of stock liquidity. EVM pools in-process (src/repeg-evm.js), Solana pools via backend/solana-spike/3_repeg.js. Runs before any buy, so the contract's minOut (oracle minus 1%) can fill.
broadcastevery cycleFor each meme and EVM chain, sends get_signed_tx(chain, nonce) for every nonce between the treasury's on-chain nonce and the meme's next nonce, and waits for the receipt. Tops up treasury gas from the keeper wallet below TREASURY_GAS_MIN_ETH.
bridgeevery cycleLists USDC transfers to the keeper on NearBlocks, re-reads each one from NEAR RPC (EXPERIMENTAL_receipt), parses the memo solidon fund <chain> <treasury>, and sends the same amount of that chain's testnet USDC to the treasury, capped by the meme's in_transit minus what was already delivered. Then proves it: credit_cash on Base (MPC-verified log, 300 TGas), report_cash on Robinhood and Solana (30 TGas). The signed delivery is saved before broadcast, so a restart resumes instead of paying twice.
treasuryevery cycle, per memeinit_treasury if no address; per EVM chain with a venue: approve_router if not approved, buy_stock if the chain holds USDC and the price is fresh, fund_chain when nothing is in transit, the chain's USDC is spent, it is the chain holding the fewest shares, and the estimated cash above the 20% buffer is at least FUND_MIN_USDC. Then rebalance (100 TGas) when the cooldown is over and the price is fresh; a no_market answer is retried at most every 600 s. Signing steps wait while a signature is in flight.
solanaevery cycle, per meme with a Solana venueinit_solana_treasury; set_solana_accounts with the canonical ATA bumps (also corrects wrong ones); keeps treasury SOL above TREASURY_SOL_MIN; checks the last signed buy first (landed: done; valid blockhash: resend; expired: resign_solana after 200 NEAR blocks, at most SOLANA_MAX_RESIGNS times); then buy_stock_solana with up to 3 tick arrays and a recent blockhash, broadcast at once; then fund_chain("solana") under the EVM rule.

The keeper never calls clear_stuck_signature. Memes on code without get_treasury_chains only get rebalance().

Indexer and API

keeper.js also starts the indexer and the read API on their own timers, so they never delay the treasury loop. --no-api turns them off; --once never starts them. node indexer.js runs them alone: that process loads no key and never signs.

Flags

node keeper.js [--once] [--dry-run] [--no-api] [--meme=a.testnet,b.testnet] [--only=oracle,broadcast,bridge,treasury,solana,repeg]
node indexer.js [--meme=...]
node repeg.js [--chain=base,robinhood,solana] [--ticker=NVDA,...] [--dry-run]
FlagEffect
--onceOne cycle, then exit. No indexer, no API.
--dry-runReal reads, no writes: every transaction is logged as DRY ... would .... No key needed.
--no-apiDo not start the indexer and its HTTP API (use when indexer.js runs separately, so only one process writes data/).
--meme=Only these memes (comma separated). Default: every meme of the factory.
--only=Only these steps. Default: all.
--help, -hPrint usage.

npm scripts: npm start (node keeper.js), npm run once, npm run dry-run (--once --dry-run), npm test.

Environment variables

The keeper reads backend/.env and contracts/.env (values already in the environment win) and never prints secrets; RPC URLs and keys are redacted from logs. Template: backend/.env.example. Every non-secret value has a testnet default.

Accounts and keys

VariableDefaultMeaning
FACTORYsolidon.testnetFactory whose memes the keeper serves
KEEPER_ACCOUNTkeeper.<FACTORY>NEAR account of the keeper: the factory's bridge and relayer
KEEPER_NEAR_PRIVATE_KEY~/.near-credentials/testnet/<KEEPER_ACCOUNT>.jsonSecret. NEAR key of the keeper account
KEEPER_EVM_PRIVATE_KEYEVM_DEPLOYER_PRIVATE_KEY from contracts/.envSecret. Keeper EVM wallet: bridge deliveries, treasury gas, re-peg (must own the MockERC20 stocks). Without it, deliveries and gas top-ups are off.
KEEPER_EVM_ADDRESSEVM_DEPLOYER_ADDRESSExpected EVM address; the keeper refuses to start if the key does not match
KEEPER_SOLANA_KEYPAIRbackend/keeper/.secrets/solana-keeper.jsonSecret. Devnet keypair, created on first run
ALCHEMY_API_KEYnoneSecret. Default RPC for Base Sepolia, Robinhood testnet and mainnet, Solana devnet
NEARBLOCKS_API_KEYnoneSecret, optional. Raises the NearBlocks rate limit
PYTH_API_KEYnoneSecret, optional. Redacted from logs
SOLIDON_ENV, SOLIDON_CONTRACTS_ENVbackend/.env, contracts/.envPaths of the two env files

RPC endpoints

VariableDefaultMeaning
NEAR_RPChttps://test.rpc.fastnear.comNEAR RPC for views
NEAR_RPC_SENDNEAR_RPCNEAR RPC for transactions. Must support broadcast_tx_commit (dRPC fails it); keep FastNear here
BASE_SEPOLIA_RPCAlchemy base-sepolia, else https://sepolia.base.orgBase Sepolia
HOOD_TESTNET_RPCAlchemy robinhood-testnet, else https://rpc.testnet.chain.robinhood.comRobinhood Chain testnet
HOOD_MAINNET_RPCAlchemy robinhood-mainnetChainlink reads. Required for the oracle step
SOLANA_DEVNET_RPCAlchemy solana-devnet, else https://api.devnet.solana.comSolana devnet
NEARBLOCKS_APIhttps://api-testnet.nearblocks.ioBridge transfer discovery

Views and transactions use separate endpoints because views are many and transactions are few but need an RPC that waits for the outcome. Production uses NEAR_RPC=https://near-testnet.drpc.org for views and NEAR_RPC_SEND=https://test.rpc.fastnear.com for transactions.

Loop and thresholds

VariableDefaultMeaning
LOOP_SECS60Cycle interval
ORACLE_INTERVAL_SECS300Oracle push interval; must stay under the contracts' 900 s staleness limit
ORACLE_MAX_FEED_AGE_SECS90000 (25 h)Older Chainlink answers are not pushed
KEEPER_SKIP_MEMESemptyComma-separated memes the keeper leaves alone (production: floor.solidon.testnet, so its cash stays on NEAR for a manual buyback and redemption demo)
FUND_MIN_USDC1Smallest fund_chain amount or chain cash the keeper acts on
TREASURY_GAS_MIN_ETH0.0002Top up an EVM treasury below this
TREASURY_GAS_TOPUP_ETH0.0005Amount sent; 0 disables top-ups
RECEIPT_TIMEOUT_SECS120Wait for an EVM receipt
CREDIT_MAX_ATTEMPTS6Retries of a failed credit_cash before it is left as credit-failed
KEEPER_STATEbackend/keeper/state.jsonBridge transfer state
TREASURY_SOL_MIN0.01Top up a Solana treasury below this
TREASURY_SOL_TOPUP0.02Top-up target
KEEPER_SOL_MIN0.1Below this the keeper requests a 1 SOL devnet airdrop
SOLANA_CONFIRM_SECS60Wait for a Solana confirmation
SOLANA_MAX_RESIGNS5resign_solana attempts for one failing buy

Re-peg

VariableDefaultMeaning
REPEG_EVERY_MIN0 (off)Run a re-peg pass every N minutes
THRESHOLD_PCT0.5Re-peg a pool whose next buy fills this many percent above Chainlink
TARGET0.995New pool price as a fraction of Chainlink
DEPTH_USD50000New one-sided stock liquidity, USD
SOLANA_PAYER_KEYPAIRbackend/solana-spike/.secrets/payer.jsonSecret. Devnet payer, mint authority of the Solana stocks
SOLANA_SPIKE_STATEthe spike's state.jsonWritable copy of the Solana spike state
DRY_RUNunsetFor repeg.js: same as --dry-run

Indexer

VariableDefaultMeaning
INDEXER_PORT8788HTTP port of the read API
INDEXER_DATAbackend/keeper/datahistory/, events/, cursor.json
INDEXER_POLL_SECS20Event poll interval
SNAPSHOT_SECSLOOP_SECSOne history point per meme per interval
FASTNEAR_TX_APIhttps://tx.test.fastnear.comFastNear transactions API

Deployment

Production runs on a VPS as two containers of one image (backend/keeper/Dockerfile, node:24-alpine, build context = repo root) from backend/deploy/compose.yml, behind a shared Caddy that terminates HTTPS for api.solidon.xyz.

ServiceCommandNotes
apinode indexer.jsRead-only, no key. INDEXER_DATA=/state/data. Caddy proxies api.solidon.xyz to solidon-api:8788.
keepernode keeper.js --no-apiSecrets from /opt/solidon/secrets.env. REPEG_EVERY_MIN=30, NEAR_RPC=https://near-testnet.drpc.org, NEAR_RPC_SEND=https://test.rpc.fastnear.com, KEEPER_SKIP_MEMES=floor.solidon.testnet, KEEPER_STATE=/state/state.json.
# ship the code, then (re)build and start both services
git archive --format=tar HEAD backend/keeper backend/solana-spike backend/deploy contracts/stocks \
  | ssh root@<vps> 'rm -rf /opt/solidon/repo && mkdir -p /opt/solidon/repo && tar -x -C /opt/solidon/repo'
ssh root@<vps> 'cd /opt/solidon/repo/backend/deploy && docker compose up -d --build'

# one re-peg pass by hand, plan only
docker compose exec keeper node repeg.js --dry-run

Host layout: /opt/solidon/state (indexer data, state.json), /opt/solidon/secrets (Solana keypairs, read-only mount), /opt/solidon/secrets.env (keys, mode 600). See backend/deploy/README.md.

Trust

StepTrust required
OracleThe mock oracle is trusted on testnet (anyone can set it). The contracts read whichever Pyth-interface oracle the factory fixed at init.
BroadcastNone. Transactions are MPC-signed and checked by the contract; anyone can broadcast.
BridgeThe testnet bridge is trusted to deliver. On mainnet this leg is NEAR Intents / 1Click.
Cash proofBase arrivals are MPC-verified. Robinhood and Solana arrivals are relayer-reported.
Treasury, Solana buysNone. Open methods; the contract builds every transaction from locked config. Wrong live Solana inputs only make a buy fail.
Re-pegTestnet only; the pools and tokens are Solidon's own.

See Security for the full threat model.

On this page