A liquidation-defense keeper that executes onchain through KeeperHub, and declines to act when the gas is not worth it.
Built for the KeeperHub Agents Onchain hackathon.
Live snapshot → the position, the decision trail on mainnet, and the KeeperHub execution history, every row linking to a block explorer. It is a snapshot rather than a live page, and says so: the real dashboard holds an API key and so cannot be served publicly.
Measured on Sepolia, 2026-08-02, four trials per cell. Method, caveats and raw data in chaos/RESULTS.md.
| Scenario | KeeperHub | ethers (blind gas limit) | ethers (default) |
|---|---|---|---|
| Gas underpricing (0.05 gwei vs ~0.98 market) | 4/4 landed | 0/4, all stuck | no data |
| Congestion (4 concurrent from one wallet) | 4/4 landed | 1/4 | 2/4 |
| Revert | all refused | all refused | all refused |
Underpricing is the clearest result and reproduces across runs. The baseline was pinned to a hardcoded 0.05 gwei with a hardcoded gas limit, which is what a static price becomes the moment the network moves. KeeperHub was given a thin gas budget and allowed to manage it, and landed everything; the blind baseline broadcast four transactions that never mined, confirmed from chain state and clearable only by same-nonce replacement at 5x market.
This measures managed execution against a static one-shot price. It is not a symmetric underbid, and RESULTS.md explains why an earlier version of this table claimed it was.
Congestion cost the baselines two and three transactions to
-32000 "already known". KeeperHub sequenced all four.
Revert shows no difference. The public RPC refuses to relay reverting transactions, so nobody burned gas and there is nothing to separate. We report that rather than dressing it up.
| Class | Meaning | Effect on the rate |
|---|---|---|
| failed / stuck | The backend under test did not deliver | Counted against it |
| prevented | Refused because the call would revert | Counted, separately |
| excluded | Our own network or RPC broke | Removed from the denominator |
That last row is why one cell reads "no data" instead of 0%. It also corrected a
real error: an earlier run scored KeeperHub 1/3 on underpricing, which read as a
reliability problem, when two of those trials were fetch failed from our own
connection. With exclusions the scenario reproduces at 4/4.
Four bugs in the harness itself, including one that would have credited an RPC outage to the baseline as a safety feature, are written up in RESULTS.md.
Reproduce with npm run chaos -- --all --n 4.
People borrow against crypto collateral. If the collateral falls far enough, anyone may liquidate them: repay part of the debt, seize collateral at a discount, and the borrower eats a 5-10% penalty.
Spotting the danger is easy. Landing the rescue transaction is not: it has to execute at 3am, during a gas spike, without being front-run. Bulwark watches positions, decides whether a rescue is economically worth its gas, and executes through KeeperHub.
Its monitoring is also listed for sale on the KeeperHub marketplace at $0.01 a call, the first half of a loop where the agent's earnings pay for its own executions. The listing is live and priced; nothing has settled through it. Marketplace settlement is mainnet-only today, so closing that loop needs real USDC on Base rather than testnet funds, and this build stayed at zero capital deliberately. Treat the revenue side as built and unexercised, not as demonstrated.
Any agent can fire a repay when a health factor dips. Bulwark computes whether the rescue is worth the gas, and declines when it is not:
HF 1.120 → ARMED. P(liquidation) 0.7% over next 24h, loss if it happens $250.00,
expected loss $1.85. Cheapest rescue REPAY at $4.44. Ratio 0.4× vs 3× margin:
holding, gas not justified yet.
Liquidation probability uses a first-passage model, not an endpoint one: a position that dips below HF 1.0 mid-window and recovers still gets liquidated, because liquidators watch every block. By the reflection principle that roughly doubles the naive estimate.
Health factor 1.10 comes out at a ~2.3% chance of touching liquidation within a day, which lines up with how often collateral actually drops 9% in 24 hours.
Two independent defenders watch one position. Either can act alone.
flowchart LR
POS[("Aave V3 position<br/>Sepolia")]
subgraph KH["Inside KeeperHub: owns CRITICAL, needs no local process"]
direction TB
TRIG["Block trigger<br/>every 50 blocks"] --> READ["Read<br/>health factor"] --> COND{"HF below<br/>1.05?"}
COND -- "no" --> STOP["Stop<br/>zero transactions"]
COND -- "yes" --> FIX["Approve LINK,<br/>supply 7 LINK<br/>onBehalfOf position"] --> VERIFY["Re-read HF<br/>to confirm recovery"]
end
subgraph AGENT["Local agent loop: owns ARMED, where declining is the point"]
direction TB
OBS["Observe over<br/>plain RPC"] --> MODEL["P(liquidation) by first passage,<br/>expected loss vs cheapest rescue"] --> DEC{"expected loss<br/>above 3x<br/>rescue cost?"}
DEC -- "no" --> HOLD["HOLD<br/>gas not justified yet"]
DEC -- "yes" --> RESCUE["RESCUE via KeeperHub<br/>approve, then repay or supply"]
end
LOG[["GuardianLog<br/>Ethereum mainnet"]]
LEDGER[/"attestations.jsonl"/]
DASH["Dashboard"]
POS --> TRIG
POS --> OBS
FIX -. "tops up collateral" .-> POS
RESCUE -. "repays or tops up" .-> POS
VERIFY --> LOG
HOLD --> LOG
RESCUE --> LOG
LOG --> LEDGER --> DASH
The split is deliberate. CRITICAL lives server-side because a keeper that stops defending when your laptop sleeps is not a keeper. ARMED lives in the agent because the first-passage model and the cost/benefit comparison do not fit in a condition node.
Both rescues and holds are attested, since the declined rescues are the interesting judgment calls. Attestation fires on a rescue or a tier change rather than every tick: a mainnet transaction every few minutes to report that nothing moved is noise, and it burns the execution quota.
The ledger exists because free RPC tiers stop serving eth_getLogs past a few
hundred blocks, so an attestation not written down as it lands becomes
unreadable within about half an hour. The chain stays the authority; the ledger
is just the index.
Autonomous liquidation defense is live, and every decision change is attested to Ethereum mainnet.
A KeeperHub workflow watches the position every 50 Sepolia blocks and rescues it with no local process running. Verified both branches on 2026-08-02:
| Health factor | Condition | Outcome |
|---|---|---|
| 1.3004 healthy | false | stopped after 3 nodes, zero transactions |
| 1.0400 critical | true | rescued to 1.3192 in 26.5s |
| What | Chain | Link |
|---|---|---|
GuardianLog deployment |
mainnet | 0x62938be3... |
GuardianLog contract |
mainnet | 0x06D8C09B... |
| Decision attestation | mainnet | 0x2d60efde... |
| Rescue: approve | Sepolia | 0xfc3555ad... |
| Rescue: supply | Sepolia | 0xc89edff9... |
The mainnet attestation carries the Sepolia rescue's hash in
remediationTxHash, so the public record and the economic action are linked.
Decoded, it reads: HF 1.0400, action AddCollateral, expected loss $4.74, rescue
cost $1.18.
KeeperHub has no deploy action, so GuardianLog went up through a CREATE3
factory, whose deploy(bytes32,bytes) is an ordinary call and therefore
reachable. The address was predicted before deploying and the on-chain runtime
code verified byte-identical to the local compile.
| Component | State |
|---|---|
| Risk tiers, probability model, cost/benefit | done, 56 tests passing |
| Remediation selection and amount math | done, verified against the live position |
Position tooling (pos:* scripts) |
done: open, fund, danger, status, evaluate |
hf-watch-critical workflow |
done, enabled, both branches verified |
GuardianLog on mainnet |
deployed, attestations landing |
| Agent loop (ARMED tier) | done: observes, decides, executes, attests, unattended |
| Marketplace listing | done, gasguard-aave-hf-check at $0.01/call (slug predates the rename and is fixed once listed) |
| Chaos harness | runnable end to end, 2 of 3 scenarios differentiate (RESULTS) |
| KeeperHub REST paths | verified by probing (agent/executor.ts) |
| Marketplace revenue loop | built, never exercised: settlement is mainnet-only and this build stayed at zero capital |
| Dashboard | done, serves both audit trails (npm run dashboard) |
| Public snapshot page | done, major101x.github.io/bulwark (npm run site:build) |
| Starter template | done, first transaction needs only an API key (README) |
| Demo video | scripted, recording pending |
The open questions this depended on (Aave V3 on Sepolia, mainnet gas sponsorship for arbitrary calls, testnet marketplace settlement, free-tier quota) were answered by the KeeperHub team on Discord and are reflected throughout.
The CRITICAL tier runs inside KeeperHub, server-side, with a fixed-size collateral top-up. It needs no local process, so the position stays defended even when our agent is down. Economics are deliberately bypassed there: at HF 1.05 a passing gas spike is not a reason to let a position liquidate.
The agent owns the ARMED tier, where the interesting behaviour is declining to act. That reasoning is too involved for condition nodes and it is where the cost/benefit model earns its place.
npm install
npm test # 56 tests, no network required
npm run typecheckThe decision logic is pure and fully testable offline: no RPC, no account, no keys.
To run the agent, copy .env.example to .env. It starts with DRY_RUN=1, which
computes and logs decisions without executing anything.
npm run agentThe agent never handles a private key. KeeperHub keeps them in Turnkey enclaves, and
the only key in this repo's config is CHAOS_BASELINE_PRIVATE_KEY, a throwaway
Sepolia key used solely by the naive baseline in the chaos harness, holding worthless
testnet funds.
agent/ decision logic (risk, remediation) + execution + attestation
chaos/ failure injectors and the reliability harness
contracts/ GuardianLog.sol, the onchain audit trail
workflows/ exported KeeperHub workflow JSON
docs/ friction log (bounty input), architecture notes
site/ generator for the public snapshot page
starter-template/ zero-to-first-transaction, for the onboarding bounty
starter-template/ is standalone: copy the folder out, npm install, and
npm run first-tx lands a real transaction with nothing but an API key. No
testnet funds, no wallet extension, no private key.
MIT