ETHEREUM SEPOLIA · 24 EXECUTIONS

gavel

Safe splits a multisig transaction into two acts that are not the same act. Signing happens off-chain, free. Executing costs gas, can be done by anyone, and is nobody's job. gavel is the party whose job it is.

Open the 10-slide deck → v1.0.25

The two acts

Signing

Off-chain, in the Safe Transaction Service. Free. The owners already did it — that is why the transaction is in the queue at all.

● Already happened
then

Executing

On-chain execTransaction. Costs gas. Permissionless — anyone may call it. Assigned to no one, so it does not happen.

▲ Nobody's job

How KeeperHub runs this

Four stages left to right. The Safe Transaction Service supplies the queue and the owner signatures. KeeperHub's Safe plugin performs four reads: the queue, nonce, threshold and owners. src/assemble.mjs decides purely and returns either ten execTransaction arguments or one of seven named refusals. KeeperHub Direct Execution simulates, then broadcasts from a wallet that owns none of the Safe. KeeperHub's own execution rows are the audit ledger.

Safe logo Safe — the queue and the owner signatures KeeperHub logo KeeperHub — the reads, the broadcast, the audit trail

Safe pluginFour reads, no writes. The queue, plus nonce(), getThreshold() and getOwners(). All seven plugin actions are reads — which is exactly why the write below has to exist.
execute/contract-callDirect Execution. Always simulate: true as a preflight, then the real call. There is no idempotency key — this page claimed one until 2026-09-09 and the API exposes none. Two identical calls both broadcast: that is what the deliberate race did, and one returned Error(GS026). Only the Safe nonce stops a double-spend.
analytics/runsThe audit ledger. KeeperHub's own execution rows carry status, durationMs and gasUsedWei, and audit.mjs renders those without computing one. receiptStatus, blockNumber and gasUsed are ours, added by --verify from the chain — this page credited them to KeeperHub until 2026-09-09.
workflows/createThe graph. Eleven nodes, ten edges, generated per chain by sync.mjs, which injects assemble.mjs verbatim into the Code node — so the function the tests run offline and the function the canvas runs on-chain cannot drift apart.

The finding this is built on

92.6% already dead

Every square is one queued Base transaction that passes the naive “threshold met, not executed” test.

Squares partition the 366; the funnel below nests them. 21 + 4 + 2 = the 27 live, and 4 + 2 = the 6 at their Safe's current nonce.

1,299Base Safes surveyed, across all of Base history
366queued transactions match the naive “ready to execute” test
339nonce already consumed — permanently dead (92.6%)
27still live
6at their Safe's current nonce
2simulate SUCCESS

A detector built on the obvious definition is ~93% false positives. Anyone can concatenate signatures; knowing which 7.4% are real is the product. Method and per-Safe detail are in the repository README.

Verify it yourself — five surfaces, none of them ours

  1. Open the transaction Status Success · 144,637 gas · block 11618878
  2. On that page open Logs A Safe ExecutionSuccess and a USDC Transfer of 10.000000 — value moved
  3. Read the Safe's contract → isOwner → 0x5E2e5Fd3aD7fDC9B94482930db8b5F45E439bab7 Returns false. The executor owns nothing on the Safe it drained.
  4. Same contract → nonce Advanced past the transaction we executed — it is genuinely consumed, not merely proposed
  5. Read the code — src/assemble.mjs is the entire decision surface npm test · 87 tests, 100% line and branch coverage on the decision surface, one of which runs against a committed real Safe Transaction Service response
The sender will look wrong, and here is why before you wonder.
KeeperHub writes are gas-sponsored, so a relayer submits and the top-level From is an address you will not recognise. But eth_getCode on our executor returns 0xef0100… — an EIP-7702 delegation designator. The delegate runs in the EOA's own context, so msg.sender at the Safe is still the executor. Verify by transaction hash, not by address history.

What is not done

No mainnet execution yet. Everything above is Ethereum Sepolia. EVIDENCE.md is absent by design, not empty by accident — scripts/audit.mjs refuses to write a row from a chain marked receiptsEligible: false.

The canvas workflow is one day old. code/run-code and HTTP Request are pro-gated and nothing discloses it before create time (issue #2279, maintainer-confirmed). A trial unlocked them on 2026-09-11; the same day the graph ran 7 times inside KeeperHub — 2 executions, 2 named refusals, 1 deliberate GS026 race loss — and went live on the Marketplace as gavel-drain at $0.05 with zero external paid calls. drain.mjs holds the 150-run track record on the Direct Execution API. Same assemble.mjs in both; we say which path produced which number.

What is done

The decision surface is finished and pure. src/assemble.mjs — zero dependencies, zero I/O, seven named pre-broadcast refusals, 87 tests at 100% line and branch coverage, one of them run against a committed real Transaction Service response rather than a fixture we invented.

The number this is built on is reproducible. 1,299 Base mainnet Safes, and all 24 published figures re-derive offline from the raw responses committed beside them — python3 survey/rederive.py, no network, no credentials. CI runs it, so the prose cannot drift from the measurement.

And it works, repeatedly. Safes drained on-chain by an address that owns none of them, and isOwner(executor) returns false for anyone who checks. Testnet, and labelled as such — but the mechanism is not a diagram.

150 executions through KeeperHub, 150 successful. Median 11.6 s from accepted to mined, p95 26.3 s, reduced from KeeperHub's own rows by python3 scripts/bench.py — stdlib only, no network, CI-run. Rehearsal figures: audit.mjs refuses to write a testnet row into evidence, and that refusal applies to these numbers too.