| name | whitehat-wallet-rescue |
| version | 1.0.0 |
| description | Rescue remaining assets from a compromised EVM wallet using legitimate
Flashbots-style searcher-sponsored bundles. Covers triage, the
sponsor-executor pattern, multi-step rescues (unstake/claim/transfer),
multi-chain alternatives, and the most common failure modes. Triggers
when a user reports a compromised wallet, mentions a sweeper bot, asks
about rescuing tokens / NFTs / staked positions / airdrops from a hacked
address, or asks for help with Flashbots bundles for asset recovery.
Also triggers when the user asks Claude to evaluate a "wallet rescue"
tool or tutorial — many are scams.
|
| license | MIT |
| allowed-tools | ["Read","Write","Edit","Bash","WebFetch"] |
Whitehat Wallet Rescue
A guide for rescuing leftover assets from a compromised EVM wallet without
losing them to the sweeper bot watching it. Based on the techniques used
by the Flashbots Whitehat group, SEAL 911 responders, and the
flashbots/searcher-sponsored-tx reference implementation.
This skill is for the case where the private key or seed is already
exposed, a sweeper bot is on the address, but value remains:
- ERC-20 tokens
- NFTs (ERC-721 / ERC-1155)
- Staked / locked positions
- Unclaimed airdrops
- Scheduled vesting unlocks
If funds have already moved on-chain to the attacker, they are gone.
On-chain transfers are not reversible. This skill only deals with what's
still in the wallet.
Step 0 — Get a human on the line first
Before writing any code, push the user toward the existing volunteer
networks. They are free, faster than a DIY attempt, and have channels
(exchange contacts, builder relationships, chain analysts) a solo
operator does not.
- SEAL 911 — Telegram
@seal_911_bot. 24/7 incident response from the
Security Alliance. Free, no recovery fees. Best first call for any
active or imminent incident.
- Flashbots Whitehat — https://whitehat.flashbots.net. The team that
pioneered this technique. They handle rescues directly; mainstream
practice for non-trivial cases.
- PhishDestroy — phishing-domain takedown side; not incident response
but pairs with SEAL.
DIY is appropriate when: the user is technical, the asset value is below
what whitehat teams typically intake (Flashbots historically wanted a
$1k+ minimum), or the rescue needs to happen now in the next few
blocks. Otherwise, ping SEAL first.
Anyone who DMs the user offering "guaranteed recovery", "Flashbots
reversal", or asks for the seed phrase to "re-sync" is a scammer. Real
responders never DM first and never charge upfront. Flag this clearly any
time it comes up in the conversation.
Step 1 — Triage the situation
Walk through these questions before touching anything:
- What chain? Flashbots covers Ethereum mainnet. Other chains need
different private RPCs (see the multi-chain section).
- What's left in the wallet? Get a snapshot via Etherscan / Debank
so you know exactly what's being rescued. Don't trust memory.
- Is there ETH in the wallet right now? If yes, it will be swept the
moment any tx is broadcast publicly. Plan accordingly.
- Is the sweeper actually running? Check the wallet's recent
transactions on Etherscan. If incoming ETH is being moved out within
the same block via a contract call, yes — assume the worst.
- Are the assets transferable? Staked / locked / vesting positions
need a multi-step rescue (unstake/claim, then transfer). Check the
contract.
- Does the user have a clean machine? If the original compromise
came from malware, the device may still be compromised. Generate the
sponsor key and rescue destination on a clean device.
Step 2 — The technique: searcher-sponsored bundles
The core problem: the compromised wallet has no ETH to pay gas, and any
ETH you send arrives publicly in the mempool, where the sweeper grabs it
before the rescue tx mines.
The solution: a Flashbots bundle containing multiple transactions
that mine atomically in one block, bypassing the public mempool.
Three accounts are involved:
EXECUTOR — the compromised wallet (private key exposed). Holds the
assets to rescue.
SPONSOR — a fresh, clean wallet funded with enough ETH to cover gas
for the whole bundle. Never reuse an old key here.
RECIPIENT — the safe destination for the rescued assets. Should be a
brand new wallet generated on a clean device, ideally a hardware
wallet. Never reuse the executor's seed.
The bundle, in order:
SPONSOR → EXECUTOR: send just enough ETH to cover the next tx(s)' gas.
EXECUTOR → token contract: transfer() / safeTransferFrom() /
whatever moves the asset to RECIPIENT. (Repeat for each asset.)
All transactions in a Flashbots bundle either mine together in the same
block or none of them mine. The sweeper can't front-run because the
bundle is submitted directly to block builders, not to the public
mempool.
There's one critical separate auth key: the Flashbots reputation
signer. This is an ECDSA key Flashbots uses to identify you as a
searcher. It does not hold funds. Use any random key; you can store it
in .env safely. It is not the executor key, not the sponsor key.
Step 3 — Pre-flight checklist
Before sending a single bundle:
Step 4 — Code template
The reference repo is flashbots/searcher-sponsored-tx (TypeScript,
~400 stars, the canonical implementation). It includes pluggable
"engines" for ERC20 transfer, ERC721 approval, CryptoKitties, etc.
Minimal working pattern (ethers v6 + @flashbots/ethers-provider-bundle).
A more complete version is in this skill's rescue-template.ts.
import { ethers } from "ethers"
import {
FlashbotsBundleProvider,
FlashbotsBundleResolution,
} from "@flashbots/ethers-provider-bundle"
const RPC_URL = process.env.ETHEREUM_RPC_URL!
const EXECUTOR_KEY = process.env.PRIVATE_KEY_EXECUTOR!
const SPONSOR_KEY = process.env.PRIVATE_KEY_SPONSOR!
const REPUTATION_KEY = process.env.FLASHBOTS_AUTH_KEY!
const RECIPIENT = process.env.RECIPIENT!
const TOKEN = process.env.TOKEN_ADDRESS!
const PRIORITY_GWEI = 3n
const BLOCKS_AHEAD = 2
async function main() {
const provider = new ethers.JsonRpcProvider(RPC_URL)
const executor = new ethers.Wallet(EXECUTOR_KEY, provider)
const sponsor = new ethers.Wallet(SPONSOR_KEY, provider)
const authSigner = new ethers.Wallet(REPUTATION_KEY)
const flashbots = await FlashbotsBundleProvider.create(
provider,
authSigner,
"https://relay.flashbots.net",
"mainnet",
)
const erc20 = new ethers.Contract(
TOKEN,
["function balanceOf(address) view returns (uint256)",
"function transfer(address,uint256) returns (bool)"],
executor,
)
const balance = await erc20.balanceOf(executor.address)
if (balance === 0n) throw new Error("Nothing to rescue")
const transferTx = await erc20.transfer.populateTransaction(RECIPIENT, balance)
const gasLimit = await provider.estimateGas({ ...transferTx, from: executor.address })
provider.on("block", async (blockNumber) => {
const target = blockNumber + BLOCKS_AHEAD
const block = await provider.getBlock(blockNumber)
if (!block?.baseFeePerGas) return
const maxBaseFee = FlashbotsBundleProvider
.getMaxBaseFeeInFutureBlock(block.baseFeePerGas, BLOCKS_AHEAD)
const maxFeePerGas = maxBaseFee + ethers.parseUnits(PRIORITY_GWEI.toString(), "gwei")
const maxPriorityFeePerGas = ethers.parseUnits(PRIORITY_GWEI.toString(), "gwei")
const gasCost = maxFeePerGas * gasLimit
const sponsorFund = {
to: executor.address,
value: gasCost,
type: 2,
maxFeePerGas,
maxPriorityFeePerGas,
gasLimit: 21000n,
chainId: 1,
}
const executorTransfer = {
...transferTx,
type: 2,
maxFeePerGas,
maxPriorityFeePerGas,
gasLimit,
chainId: 1,
}
const signedBundle = await flashbots.signBundle([
{ signer: sponsor, transaction: sponsorFund },
{ signer: executor, transaction: executorTransfer },
])
const sim = await flashbots.simulate(signedBundle, target)
if ("error" in sim) {
console.error("sim error:", sim.error.message)
return
}
console.log("simulation ok, sending bundle for block", target)
const submission = await flashbots.sendRawBundle(signedBundle, target)
if ("error" in submission) {
console.error("submission error:", submission.error.message)
return
}
const resolution = await submission.wait()
if (resolution === FlashbotsBundleResolution.BundleIncluded) {
console.log("RESCUED in block", target)
process.exit(0)
} else if (resolution === FlashbotsBundleResolution.BlockPassedWithoutInclusion) {
console.log("missed block", target, "— retrying")
} else {
console.log("nonce too high — bundle already mined?")
process.exit(0)
}
})
}
main().catch(console.error)
A full version with proper module structure, multi-asset support, and
NFT engines is in rescue-template.ts alongside this skill.
Step 5 — Multi-step rescues (the more interesting case)
Single transfers are the easy case. Real rescues usually involve doing
something else first to unlock the asset. The bundle handles this
naturally — just keep adding transactions in order.
Pattern: airdrop claim + transfer (Kane Wallmann's ENS rescue, ~$13k):
- Sponsor → Executor: ETH for gas
- Executor → Airdrop contract:
claim()
- Executor → Token contract:
transfer(safe, amount)
Pattern: staking unwind:
- Sponsor → Executor: ETH for gas (more, since unstaking is expensive)
- Executor → Staking contract:
unstake() / withdraw() / exit()
- Executor → Token contract:
transfer(safe, balance)
Pattern: NFT rescue (ERC-721):
Two routes:
safeTransferFrom(executor, recipient, tokenId) — simplest
- For NFTs trapped behind an approval setup (rare): set
setApprovalForAll
for a helper, then helper pulls them. Use the Approval721 engine from
searcher-sponsored-tx as a template.
Pattern: Gelato ICO recovery (mouseless0x/Flashbots-Recovery, $44k):
Required seed + claim + withdraw all in the same block. The block
atomicity of bundles is what makes this work.
Each step is just another signed transaction in signBundle. Order
matters; nonces must increment correctly per signer.
Step 6 — Multi-chain alternatives
Flashbots is Ethereum-mainnet only. For other chains, the same idea
(private mempool + bundle) needs a different relay:
| Chain | Private bundle endpoint | Notes |
|---|
| Ethereum | https://relay.flashbots.net | Canonical. Also: Beaverbuild, Titan, rsync-builder, BloXroute. |
| BSC | BloXroute Cloud-API, 48 Club bundle relay | 48 Club's Puissant API is the closest BSC analog. |
| Polygon PoS | BloXroute, Marlin private relay | Sweeper bots are common here too. |
| Arbitrum | Centralized sequencer (FIFO) | No private bundle. Race the sweeper on gas instead, or use the sequencer's express lane if eligible. |
| Optimism | Centralized sequencer (FIFO) | Same as Arbitrum. Limited DIY options. |
| Base | Centralized sequencer (FIFO) | Same. |
| L2s post-decentralization | Builder market expected; relays will emerge | Track per-L2. |
On rollups with a centralized FIFO sequencer, there is no public
mempool to front-run — but the sweeper can still race you on regular RPC
submission. Strategies:
- Submit the rescue and the gas-funding tx through the same RPC, in the
same TCP burst, with priority fee well above what the sweeper is
paying.
- Some sequencers (Arbitrum Nova, MegaETH) offer paid express lanes.
- Failing that: send funding via an internal contract call so it doesn't
appear as a plain
to: executor tx the sweeper recognizes.
Step 7 — Why bundles fail to land
When a bundle doesn't include, it's usually one of:
- Priority fee too low. Block builders take the most profitable
bundle for that slot. Bid higher.
- Bundle reverts in simulation. Always simulate first. A bad nonce,
insufficient approval, contract pause — anything that reverts kills
the whole bundle.
- Builder market share. Flashbots has historically had ~80-90% of
blocks, but not 100%. Submit to multiple builders (Beaverbuild,
Titan, rsync) to maximize coverage.
- Sweeper countered with a higher-fee public tx. Rare in bundle
context but possible if your sponsor-funding amount overshoots and
the sweeper notices it after the fact. Send minimum required gas.
- Nonce conflict. Both signers need correct nonces relative to
chain state. If something else from the executor address goes
through first, the whole bundle reverts.
- Reorgs. If the bundle lands but the block reorgs, ideally all
transactions appear together in the new block. Using the same gas
price across the bundle (not coinbase tips) reduces fragility under
reorg.
getBundleStats(bundleHash, targetBlockNumber) returns timing info
about how far through the relay → builder → validator pipeline the
bundle got, which usually points at the cause.
Step 8 — Scam red flags to flag immediately
When evaluating a "wallet rescue" tutorial, repo, or service, treat any
of the following as proof it's malicious:
- Asks for the private key or seed in a config file. The legitimate
technique does require the executor key, but only locally on the
user's own machine, in their own script. A tool that asks you to
paste your key into a website or run a closed-source binary is
stealing it.
private_keys (plural) in .env. No legitimate setup needs more
than the one executor key.
- GitHub repo not under
github.com/flashbots/* but claims to be
"official Flashbots". Real Flashbots tools are at github.com/flashbots.
- Tells you to "send a small amount of ETH first" to the compromised
wallet without using a Flashbots bundle. That ETH is the sweeper's
next meal.
- "DMed you to help" on Telegram / Discord / Twitter. SEAL, Flashbots,
ZachXBT, PhishDestroy never DM first.
- Demands payment upfront or guarantees recovery. Real whitehats work
on success-fee or are entirely free.
- "Recovery contract" asks you to approve unlimited spending. Reading
the contract: if it's an
Ownable contract with a single recovery
function the owner can call to pull tokens, that owner is the scammer.
- Asks the user to install a browser extension or download an
executable to "scan" the wallet.
The Medium article that triggered this skill (the "ICanSee Flashbot
tutorial") hits items 1, 2, and 3. It is a private-key exfiltration scam
dressed up as a tutorial.
Step 9 — Operational notes Claude should follow
When walking a user through a rescue:
- Confirm chain, asset list, and remaining ETH state before generating
any code. Don't assume Ethereum mainnet just because they said
"wallet".
- Push SEAL 911 and Flashbots Whitehat first. Always. DIY is a fallback.
- Never ask the user to paste a private key or seed into the
conversation. They keep keys in their own environment; Claude only
ever sees addresses, tx hashes, contract addresses.
- If the user does paste a key by mistake, advise them to treat that key
as fully burned, generate a fresh sponsor + recipient, and proceed
with the now-doubly-compromised executor as the rescue target.
- Generate fresh code per situation. Don't reuse a stale template
without checking the engine matches the asset type (ERC-20 vs ERC-721
vs staking-protocol-X).
- Always include
simulate() before sendRawBundle() in any generated
script.
- Be honest about odds. If the wallet has been compromised for weeks
and the sweeper is well-funded, the rescue may fail. Don't oversell.
References
Primary sources for the technique:
Incident response: