Running a keeper
Nothing in QuaiAxe requires a privileged operator. collect, collectMany, collectAndSweep, and
sweep are all callable by anyone, on any deposit or vault. "The keeper" is just the name for
whichever process is actually calling them — Hartii runs one, and anyone else is free to run their
own against the same public entry points.
Why it's economically viable to run one
Every collection and every sweep pays the caller a bounded, measured gas refund — see burn-vault for the exact mechanics. On real Quai gas, that refund covers roughly 80% of the true cost (measured 0.79–0.85× on Orchard; the constants are deliberately conservative so a refund is never computed above 1.0× in the test suite). A keeper running purely for the refund alone is running at a small loss, by design — the refund exists to make running a keeper cheap, not to make it profitable on its own.
What actually pays for a keeper is the 0.5% treasury fee (FEE_BPS) that accrues on every
gross QUAI a keeper's collections and sweeps move through the vault — but only to whichever address
is the treasury. Running the keeper with the treasury's own key turns "≈80% gas refund" plus
"0.5% of everything moved" into a net-positive operation: e.g. a ~855 QUAI slice earns ≈4.3 QUAI of
fee against ≈2 QUAI of unrefunded gas. Running it with any other key is running at a real, small,
bounded loss to subsidize the burn.
The planner's decision rules (scripts/lib/keeperPlan.mjs)
Hartii's reference keeper (command-center/scripts/vault-sweep.mjs) never sends a transaction
without first asking a pure, network-free planner whether it's worth it. All money math is BigInt
wei throughout — never floating point near a value comparison.
refundPrice(gasPriceWei, basefeeWei)mirrorsQaxeBurnVault.refundPrice()exactly (min(gasPrice, 2*basefee)), so a plan built from a stale or inflated gas price can never claim more than the contract would actually pay.measuredRefund(...)takes the projected on-chain refund (gas units ×refundPrice, capped by the action's own allowance) and further discounts it by a configured measured ratio (default7900= 0.79×) — the keeper never assumes a refund reaches its theoretical on-chain cap, because Quai's real gas pricing doesn't let it.evaluateDeposit(...)— is onecollectworth it on its own?net = refund + feeBenefit - marginalGasCost, wherefeeBenefitis only counted whenconfig.signerIsTreasuryistrue. Included only ifnet >= -tolerance(default tolerance0, i.e. strictly non-negative).planDeposits(...)ranks and filters a batch: the shared transaction overhead (TX_BASE_GAS+ calldata cost) is attributed once, to whichever deposit ends up first in the included set — mirroring_collectMany's own "overhead once, to the first success" rule — so a small deposit at the front of a batch never silently borrows another deposit's refund. A losing deposit is simply excluded; the batch never leans on one payer to carry the rest.evaluateSweep(...)— samenetlogic againstpreviewSweep()'srefundReserveandfeeWei. Refuses immediately ifcanSweep()isfalse(cooldown or below minimum) regardless of the numbers.evaluateForceOnceV1Sweep(...)— the V1 probe vault (MinerBurnVault) has no refund mechanism in the contract at all. This function never returns "do it" on its own; it exists purely to print the honest expected loss before an operator deliberately opts in with--force-once. See v1-probe.evaluateWithdrawFees(...)—withdrawFees()pays only the treasury, never the caller, so pulling it is deliberate housekeeping, gated behind its own configured threshold (minFeesOwedQuai) and its own, usually more generous, loss tolerance (housekeepingLossToleranceQuai) — never sent silently on every run.checkStuckVault(...)— if a vault has sat above its actionable threshold forstuckHours(default 6) with nothing collected, this fires exactly one alert per "stuck episode" (tracked from the keeper's own append-only ledger, not a separate state file that could fall out of sync).
Every simulated call (estimateGas/staticCall) runs before anything is planned — a revert costs
only an RPC round trip. Every send and every reverted on-chain attempt is logged as its own
line in command-center/reports/revenue/vault-sweep-ledger.jsonl, because the public "sweep now"
button on the site and a keeper can race for the same sweep, and a losing attempt still spends real
gas that needs a record.
V1 is excluded from unattended runs
PROBE_VAULT_V1_ADDRESS (0x0003Dc0349B75ee1A81FE25310BA420a6aaB5938, the V1 probe)
is hardcoded-excluded from every unattended keeper pass — belt-and-suspenders on top of the
"V1 refuses without --force-once" guard already built into the planner, so no config edit or bug
in that guard alone can put it into a scheduled run. It is only ever swept deliberately:
node scripts/vault-sweep.mjs --execute --vault 0x0003Dc0349B75ee1A81FE25310BA420a6aaB5938 --force-onceThis prints the expected signer loss (pure gas, no refund) before sending anything.
Running your own keeper
Everything a keeper needs is public and permissionless:
- Enumerate the deposit addresses you know about (your own site's records, or scan
Collectedevents onQaxeDepositFactory). - Read their balances and the vault's readiness in as few round trips as possible with
QaxeLensandvault.canSweep()/vault.previewSweep(). - Simulate before sending:
estimateGas/staticCalleverycollect/collectAndSweep/sweepyou're considering. - Decide with real numbers — reimplement (or import) the logic above, and remember: unless you are the treasury, you should expect to run at a small, bounded loss per action, subsidizing the burn. That is a legitimate, deliberate choice; just don't expect the refund alone to cover it.
- Send, and keep your own record of what you sent and what reverted — a reverted send still spends real gas.
See integrate for the exact calls (collect, previewSweep+sweep with a real
slippage floor, QaxeLens.read).