Built for Agents Onchain — KeeperHub × DoraHacks

Revoker

Takes the approval away before it can be used.

$revoker watch

TypeScript · Solidity 0.8.28 + Foundry · viem · KeeperHub · Ethereum Sepolia

The most common wallet-drain vector

approve(spender, MAX_UINT256)

You signed it once, months ago. It never expires — and the day that contract turns malicious, the allowance is still live.

The problem

The tools that act need a human to authorise them.

Automated revoking now ships for consumer wallets — Revoke.cash Ultimate, MetaMask Advanced Permissions. Every one routes through a human granting a permission in a wallet UI. A keeper wallet has no human and no wallet UI, so that funnel never reaches it.

APPROVAL RISK — HIGH

A spender you approved has been flagged. Your allowance is unlimited.

allowance = MAX_UINT256 · granted 214 days ago

Acknowledge

For a headless signer the state of the art ends here — a notification, and a button that does nothing on-chain.

The solution

Revoker acts.

An autonomous agent that watches a wallet’s live approval set and executes approve(spender, 0) through KeeperHub the instant a threat rule fires — no human in the loop.

revoker — watch · Ethereum Sepolia
$ revoker watch
watching 0x5E2e…bab7 · 4 rules + 2 holds armed · signer: Turnkey enclave via KeeperHub
[detect]  MockUSDC approval → 0x8eBf…892e · allowance = MAX_UINT256
[rule]    unlimited-to-unverified — FIRED (spender source unverified)
[rule]    denylisted — FIRED (known-bad spender)
[execute] KeeperHub check-and-execute → approve(spender, 0)
[confirm] 0xc45a19a6…a86d7c · allowance = 0 · sponsored · no human involved

Proof of execution · Ethereum Sepolia

A real drain, really stopped.

The trap

Keeper wallet grants unlimited approval

approve(spender, MAX_UINT256) to the drainer contract.

0x99227905…0cda6f allowance = 1.157e77
The response

Revoker fires — no human involved

approve(spender, 0) via KeeperHub check-and-execute.

0xc45a19a6…a86d7c allowance = 0
The verdict

The drainer fires anyway

The drain tx succeeded — it just found nothing left to take.

We let the drain contract fire after the revoke. It succeeded — and

took 0

balance unchanged: 10,000 mUSDC
across the whole sequence

Watch the 2:34 demo ↗ The recorded run of this exact cycle — the drain fires at the end, and takes nothing.

How it works

One loop. No human.

Wallet approval state Ethereum Sepolia approvals · balances · code watched: 0x5E2e…bab7 poll Revoker agent watcher — live approval set rules — 3 auditable checks planner — approve(spender, 0) TypeScript · viem · holds no key execute KeeperHub ONE ATOMIC OPERATION re-read allowance — live chain ↓ execute approve(spender, 0) Turnkey enclave sponsored relay signing never leaves the enclave tx The revoke lands Ethereum Sepolia allowance → 0 gas 46,482 (p50) audit trail — execution id · status · gas · sponsorship → local /verify dashboard (SSE)
Second ledger

That loop runs over two allowance ledgers, not one. Permit2 — the shared approval contract most of the ERC-20 ecosystem routes through — keeps its own. A grant written there changes nothing in the token and emits no Approval event, so every watcher built on those logs is blind to it by construction. Revoker reads both, and revokes Permit2 with lockdown(): however many slots, one transaction.

The sharpest technical idea

No stale revoke, ever.

read-then-write a typical agent

read allowance→ 1.157e77
stale-read window A mempool-watching drainer slips a transferFrom in here — before the revoke lands.
approve(spender, 0)
Decides at time T, acts at T+n. The window is the attack surface.

check-and-execute Revoker, via KeeperHub

read allowancelive chain state
approve(spender, 0)
one atomic server-side operation
Same two steps, no gap between them. The window we open is closed.
KeeperHub’s receipt from the reference run — the condition re-read server-side at execution time: allowance re-read server-side, then zeroed — one operation = 2256 − 1 — MAX_UINT256, read from the live chain, not passed in by this process.

Integration depth

KeeperHub is the engine.

11 API surfaces 21 call sites 4 threat rules + 2 holds

Remove KeeperHub and Revoker needs seven separate systems — relayer, gas oracle, MEV-protected route, confirmation poller, action discovery, ABI resolution, audit pipeline — plus custody. The atomic check-and-execute and the enclave-held key are the two pieces a standalone script cannot honestly replicate.

No key to steal. Signing happens inside a Turnkey enclave via KeeperHub — the agent process never holds a private key.

POST /execute/check-and-executethe atomic revoke
POST /execute/contract-callcontract writes
POST /execute/transfernative transfers
GET  /execute/{id}/statusconfirmation · gas · audit record
GET  /chainsnetwork + explorer resolution
GET  /chains/{id}/abisource-verification signal, rule 1
GET  /user/walletsigner identity assertion
GET  /user/wallet/balancestoken discovery
GET·POST·PATCH /workflows3 endpoints — tooling, not the agent
simulate: truepre-flight dry runs
Idempotency-Keysafe retries, no double-execution

Reliability · 25 full cycles against live Sepolia

Measured, not claimed.

13.47smedian detect → revoke
25.17sp95
25/25cycles succeeded
46,482gas per revoke (p50 = p95)

Every bar is a real transaction. Cycles 1, 13 and 18 hit a slow block-inclusion window (amber) — the network’s variance, reported as a distribution, not hidden behind an average.

835 tests — 743 TypeScript · 54 Solidity · 38 E2E 100% Solidity coverage — lines · statements · branches · functions

Scope · stated plainly

Known limits, on the record.

A security tool that hides its blind spots is part of the problem. These are Revoker’s.

Sepolia testnet only

The mainnet path is documented, not executed. No real funds were risked producing any number in this deck.

Token discovery is watchlist-scoped

No public RPC serves an address-less eth_getLogs over a useful range. Revoker protects the tokens it is told to watch — not coverage it doesn’t have.

young-spender abstains loudly

Without an archive RPC it returns INDETERMINATE and names the remedy — instead of silently reporting “safe”.

The threat model is narrow on purpose

A spender that is verified, aged, and off the deny-list trips nothing. That case is out of scope, not silently mishandled.

The ask

Open the receipts.

Judge Revoker on execution — that is the point. Every number in this deck is a Sepolia transaction; the allowance we zeroed is still zero, and one cast call proves it.

# no credentials, no account, no trust in this deck required
cast call 0x4facb5FD1682c4449cAD42b7590861f7eD5c88Cb \
  "allowance(address,address)(uint256)" \
  0x5E2e5Fd3aD7fDC9B94482930db8b5F45E439bab7 \
  0x8eBf8540EdE8e40CD94825C418758d4029D8892e \
  --rpc-url https://ethereum-sepolia-rpc.publicnode.com

Expected output: 0

Permit2 lockdown(), same story on the second ledger arm upstream · 11445297 Permit2.approve · 11445298 lockdown() · 11445392
revoker.edycu.dev

The drainer fired.
It found an empty room.