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.
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 happenedOn-chain execTransaction. Costs gas. Permissionless — anyone may call it. Assigned to no one, so it does not happen.
Safe — the queue and the owner signatures
KeeperHub — the reads, the broadcast, the audit trail
| Safe plugin | Four 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/ | Direct 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/ | The 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/ | The 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. |
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.
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.
isOwner →
0x5E2e5Fd3aD7fDC9B94482930db8b5F45E439bab7
Returns false. The executor owns nothing on the Safe it drained.
nonce
Advanced past the transaction we executed — it is genuinely consumed, not merely proposed
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
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.
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.
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.