Hartii developer docs

QuaiAxe

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) mirrors QaxeBurnVault.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 (default 7900 = 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 one collect worth it on its own? net = refund + feeBenefit - marginalGasCost, where feeBenefit is only counted when config.signerIsTreasury is true. Included only if net >= -tolerance (default tolerance 0, 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(...) — same net logic against previewSweep()'s refundReserve and feeWei. Refuses immediately if canSweep() is false (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 for stuckHours (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:

bash
node scripts/vault-sweep.mjs --execute --vault 0x0003Dc0349B75ee1A81FE25310BA420a6aaB5938 --force-once

This prints the expected signer loss (pure gas, no refund) before sending anything.

Running your own keeper

Everything a keeper needs is public and permissionless:

  1. Enumerate the deposit addresses you know about (your own site's records, or scan Collected events on QaxeDepositFactory).
  2. Read their balances and the vault's readiness in as few round trips as possible with QaxeLens and vault.canSweep() / vault.previewSweep().
  3. Simulate before sending: estimateGas/staticCall every collect/collectAndSweep/sweep you're considering.
  4. 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.
  5. 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).

Quai Network mainnet · chain 9 · Cyprus-1. Figures marked "read on" a date were read from the chain that day; re-read before relying on them.