Revoker Ethereum Sepolia

Autonomous on-chain security agent · built on KeeperHub

The agent that takes the approval away before it can be used.

Token approvals are the most common wallet-drain vector, and the tools that revoke them automatically all start the same way: a human connects a wallet. A keeper wallet never can. Revoker is a self-hosted policy engine for wallets with no human at all — the moment a threat rule fires it executes approve(spender, 0) through KeeperHub.

Every number on this page links to a transaction you can open.

13.47s median detect → revoke
(p95 25.17s)
25/25 benchmark cycles
succeeded
46,482 gas per revoke (p50)
sponsored 25/25

Proof of execution

A real drain, really stopped.

The full cycle, executed on Ethereum Sepolia. Three transactions, in order. Open each one.

  1. The trap

    Keeper wallet grants unlimited approval

    The everyday act that arms every drain: approve(spender, MAX_UINT256) to a spender contract. Allowance jumps to 1.157e77 — effectively infinite.

    0x99227905…0cda6f allowance = 1.157e77
  2. The response

    Revoker fires — no human involved

    Two rules trip — unlimited-to-unverified and denylisted. Revoker executes approve(spender, 0) through KeeperHub's atomic check-and-execute, with no human in the loop.

    0xc45a19a6…a86d7c allowance = 0
  3. The verdict

    The drainer fires anyway

    The drain transaction succeeded. It did not revert. It was not blocked. It ran exactly as its author intended — and it found nothing to take, because the approval was already gone.

Step 3 is the whole story. Revoker doesn't intercept the attack — it makes the attack find an empty room.

took 0

watched wallet's balance unchanged: 10,000 mUSDC across the whole sequence

Reading the explorer links: KeeperHub executes through a sponsored relay, so the from address is KeeperHub's relayer — not the signer. The flow is relayer 0x809d…0444 → forwarder 0x5af5…f07d → Turnkey account 0x5E2e…bab7. The signer appears in the calldata; this is expected, not an anomaly.

Don't take our word for it

Check the allowance yourself, right now.

One cast call against a public RPC. No credentials, no account, no trust in this page required.

cast call 0x4facb5FD1682c4449cAD42b7590861f7eD5c88Cb \
  "allowance(address,address)(uint256)" \
  0x5E2e5Fd3aD7fDC9B94482930db8b5F45E439bab7 \
  0x8eBf8540EdE8e40CD94825C418758d4029D8892e \
  --rpc-url https://ethereum-sepolia-rpc.publicnode.com

Expected output: 0 — the allowance Revoker zeroed is still zero.

The technical differentiator

No stale revoke, ever.

Most agents read state, decide, then write. That gap between deciding and acting is exactly what a mempool-watching drainer needs.

read-then-write a typical agent

read allowance→ 1.157e77
stale-read window The drainer sees the pending revoke in the mempool and 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; the mempool window is not ours to close.

The condition is evaluated against the live chain, not a value this process passed in. On the reference run KeeperHub reported, at execution time:

condition re-read server-side at execution time, then executed in the same operation
= 2256 − 1 — the MAX_UINT256 allowance, re-read server-side.

No key to steal. Signing happens inside a Turnkey enclave via KeeperHub — the agent process never holds a private key. An autonomous agent with a hot key is a liability; one that can only ask a policy-bound signer to act is not.

The second ledger

The approval an ERC-20 watcher cannot see.

Permit2 is the shared approval contract most of the ERC-20 ecosystem now routes through, and it keeps its own allowance ledger. A grant written there never touches the token contract — no Approval event, no change to the token's own allowance mapping.

watch ERC-20 Approval logs every approval tool — including this one, until recently

grant written into Permit2→ its own ledger
structurally blind The token emits no Approval event and its own allowance mapping never changes. This isn't a missed alert — there is no event to subscribe to.
spender pulls funds through Permit2
A live, unlimited approval that the watcher reports as absent.

read Permit2's ledger Revoker, second watch path

Permit2 Approval / Permit / Lockdown logs
lockdown([…pairs])N slots
one transaction, however many slots
The ERC-20 path pays a base fee per exposure. lockdown() takes an array and clears them all at once.
52,213 gas, sponsored 16.2s detect → confirmed 1 Lockdown log, emitted by canonical Permit2 MAX_UINT160 → 0 slot amount
The Permit2 lockdown, proved on Ethereum Sepolia
#StepBlockTransaction
1 Upstream approve(PERMIT2, MAX) — the enabling grant 11445297 0xa52cb025…5855a2
2 Permit2.approve — arm the threat in Permit2's ledger 11445298 0xe978f12f…c73297
3 lockdown() — the revoke 11445392 0x20d70cf1…6ba124

Reading step 3. Open the Logs tab — exactly one entry, a Lockdown event emitted by canonical Permit2 itself (0x000000000022D473030F116dDEE9F6B43aC78BA3), and the slot's amount went from MAX_UINT160 to 0. The block column is there for the same reason as the one above: 11445297 → 11445298 → 11445392 is one sequence you can check rather than take on trust.

The detour that made this land. The guard first read Permit2's own allowance(), which returns three values. The check-and-execute condition is {operator, value} with no tuple selector, so it had no scalar to compare: it scored gt 0 as false, skipped the write, and logged a tidy success while the grant sat there fully armed. Only a real transaction could have caught that.

The fix is a 723-byte ownerless, storageless view (deployed at block 11445389, after the threat was armed — which is why it sits outside the sequence above) that flattens the tuple to a single uint160. The action still calls canonical Permit2, and check and action still sit inside the same operation, so the no-front-run property holds here too. If the helper is missing the agent refuses to submit rather than send an unguarded lockdown.

Detection

Four rules, two holds. No model.

Concrete, auditable threat conditions. Every firing carries its evidence into the audit trail, so a revoke can be justified after the fact.

unlimited-to-unverified

A MAX_UINT256 allowance granted to a contract whose source is unreadable — infinite trust in code nobody can inspect.

signal: KeeperHub ABI resolution, live

young-spender

The spender contract was deployed less than 7 days ago. Fresh contracts holding spend rights are a classic drain setup.

signal: eth_getCode + binary search

denylisted

The spender is on the known-bad list. No further debate — a confirmed bad actor with an open allowance gets revoked.

signal: data/denylist.json

permit2-long-lived

A Permit2 allowance still valid more than 30 days out, held by a contract whose source is unreadable. An expired one is left alone — the transfer would revert anyway.

signal: Permit2 ledger + block timestamp

Any one rule firing is sufficient. These are independent signals of different kinds, not weighted terms in a score — requiring consensus would mean ignoring a confirmed deny-list hit because the contract happened to be verified. And deliberately not an ML risk model: an agent that moves funds on an opaque score is not auditable, and “the model said so” is not a defence when it is wrong.

Benchmark

Measured, not claimed.

25 full detect→revoke cycles against live Sepolia, each one confirmed by reading the chain — not by trusting the execution report.

Latency distribution over 25 cycles
Metricp50p95minmax
response — detection → revoke confirmed 13.47s25.17s9.31s26.55s
exposure — threat live → revoke confirmed 13.71s25.55s9.72s26.71s
cycles: 25/25 succeeded gas per revoke: 46,482 (p50 = p95) sponsored: 25/25
Response time per cycle — every bar is a real transaction; click one. Cycles 1, 13 and 18 hit a slow block-inclusion window (amber).

What these numbers do not include. The benchmark triggers detection immediately rather than waiting for the poll timer, so a deployment polling every pollIntervalMs adds an average of pollIntervalMs/2 on top.

The p95 is nearly double the p50 because three of the twenty-five cycles hit a slow block-inclusion window — that variance is the network's, not the agent's, which is exactly why this is reported as a distribution instead of a headline number. Full per-cycle table: BENCHMARK.md in the repo.

Integration

KeeperHub is the engine, not decoration.

11 distinct API surfaces 21 application call sites 835 tests 4 threat rules + 2 holds

Remove KeeperHub and Revoker needs seven separate systems: a transaction relayer, a congestion-aware gas oracle, an MEV-protected submission route, a confirmation poller, an action-discovery layer, an ABI resolution service, and an audit-log pipeline — plus a custody solution. The sharpest dependency isn't convenience; it's that the atomic check-and-execute and the enclave-held key are things a standalone script cannot honestly replicate.

KeeperHub API surfaces used by Revoker
SurfaceUsed for
POST /api/execute/check-and-executethe atomic revoke
POST /api/execute/contract-callcontract writes, arming the demo approval
POST /api/execute/transfernative transfers
GET /api/execute/{id}/statusconfirmation, gas, sponsorship, audit record
GET /api/chainsnetwork + explorer resolution
GET /api/chains/{id}/abisource-verification signal for rule 1
GET /api/user/walletsigner identity assertion
GET /api/user/wallet/balancestoken discovery (curated registry)
GET /api/workflowsworkflow listing — tooling, not the agent
POST /api/workflows/createpublishes the workflow definition
PATCH /api/workflows/{id}updates it in place on re-run
simulate: truepre-flight dry runs before spending gas
Idempotency-Keysafe retries without double-execution
gasLimitMultiplierthe fee ladder’s two rungs
X-Poll-Interval-Hintserver-paced status polling

Scope

Known limits, stated plainly.

A security tool that hides its blind spots is part of the problem. These are Revoker's, on the record.

  1. Sepolia testnet only

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

  2. Token discovery is watchlist-scoped

    No public RPC serves an address-less eth_getLogs over a useful block range. Revoker protects the tokens it is told to watch, rather than implying coverage it does not have. Production would resolve the set from an indexer.

  3. young-spender needs an archive RPC

    Without one it returns INDETERMINATE and names the remedy — loudly — instead of reporting “safe”. A threat rule that silently degrades into a rubber stamp is worse than one that admits it cannot see.

  4. The threat model is narrow on purpose

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

  5. The /verify dashboard runs locally

    A continuous watcher cannot be a serverless function, so the live dashboard is served from the same long-lived process that does the watching.

Demo

Watch the full cycle.

2:34 · the exact cycle proven above — seed the threat, watch Revoker detect and revoke it, then let the drainer fire anyway. Watch on YouTube ↗