KESTREL

How it works

An overnight desk that reads the chain itself.

No indexer, no cached price database, no mock data. Every figure is read from Base and Robinhood Chain at request time, with its block number and an explorer link.

Base 8453·Robinhood Chain 4663·READ → SCORE → SIMULATE → RESCUE → PAGE

The problem

Why Base and Robinhood Chain need overnight defense now

Onchain risk does not keep office hours. Three things go wrong while you sleep, and all three are now live on the two chains Kestrel watches.

03:42 UTC: an approval you forgot gets drained

Unlimited ERC-20 and Permit2 allowances outlive the dApps you granted them to. When one of those spenders is compromised, revoking eight approvals one wallet popup at a time, at 3am and mid-exploit, fails halfway. The attacker needs one block; you need eight signatures.

Two chains: bridged supply can quietly lose its backing

Base and Robinhood Chain are linked by a LayerZero OFT for $VIBES. The same asset trades in two pools on two chains, and the two supplies must reconcile to the wei. If circulating supply on Robinhood ever exceeds what is locked on Base, someone minted unbacked tokens. Nobody checks that invariant block by block, so Kestrel does.

72 hours: backers miss the only window that matters

On Vibestarter, founders unlock each 15% tranche publicly and backers have exactly 72 hours to challenge it. That window routinely opens while half the community is asleep. Most backers find out after the money has moved.

Read

Every number comes off the wire.

Kestrel reads eth_getCode, EIP-1967 storage slots, getReserves and slot0, ERC-20 and Permit2 allowances, and Transfer and Approval logs straight from public Base and Robinhood Chain RPCs. Vibestarter campaign metadata comes from its public API and MCP server. Nothing is pre-indexed and nothing is interpolated: if a read fails, the cell says so.

Robinhood Chain's Cloudflare WAF rejects default bot user agents, so server-side clients send a named Kestrel-Sentinel/1.0 agent. Base reads fall back from mainnet.base.org to a second public endpoint, and both chains batch through Multicall3.

Score

Findings that add up to a score.

Deterministic, documented rules turn findings into a 0–100 Overnight Threat score. Every finding carries explicit points: a mutable proxy with an EOA admin is +25, a single-key owner of a contract with privileged selectors +18, each unlimited approval +10, LP depth down 15% overnight +15, an open challenge window +15, an unbacked bridge +30.

The score is their clamped sum, so every point on screen traces back to a block and a link. If no language model is configured, the Morning Desk Brief is written by rules from the same findings.

Simulate

Stress the exit before it happens.

The Rug Lab replays the night's worst cases against live pool state: the top whale dumps 100%, half the liquidity is pulled, the OFT bridge rate limit saturates. All arithmetic is BigInt, from Aerodrome reserves and Uniswap v3 sqrtPriceX96 and liquidity. The formulas are in AMM math below.

Rescue

One signature, simulated first.

The Panic Bunker batches every revoke and an optional unwind to USDC into one EIP-5792 wallet_sendCalls bundle on smart wallets, and steps EOAs through the same calls one by one. Every call is simulated before the wallet is asked to sign. The procedure is in EIP-5792 rescue.

Page

A pager that opens the bunker.

Night-Shift Pager watches fire on thresholds you set: LP down more than 15%, cross-chain spread above 3%, a 72-hour challenge window opening. Each alert goes to a webhook or Telegram with a deep link such as /rescue?target=0x…&chain=8453, which opens the Panic Bunker already loaded with the target. Try it on a live Safe: the Send treasury rescue preview.

Mechanics

Architecture

One server-side engine reads both chains; the browser only ever signs. The same engine backs the web desk, the agent gate, the MCP server and the pager.

stackserver: viem · browser: wagmi
Next.js 15 App Router (SSR: / · /app · /how-it-works · /agents · /ecosystem · /rescue)
│
├── Onchain engine (viem, server-side)
│   ├── Base client      mainnet.base.org → publicnode fallback · multicall batching
│   ├── Robinhood client rpc.mainnet.chain.robinhood.com (+ clean User-Agent for WAF) · Multicall3
│   ├── xray.ts     eth_getCode opcode walk · EIP-1967 slots · owner()/Safe posture
│   ├── pools.ts    Aerodrome getReserves / Uni v3 slot0+liquidity · 8h deltas
│   ├── wallet.ts   ERC-20 allowance() · Permit2 allowance() · Approval logs (8h)
│   ├── flow.ts     Transfer logs (8h) → volume, largest transfer, top active holder
│   ├── parity.ts   OFT 1:1 invariant · cross-venue spread · gas drag
│   ├── escrow.ts   VibesTranchEscrow (uint8 tranche ABI) + Vibestarter API enrichment
│   └── findings.ts deterministic scoring → 0–100 · NOMINAL / ELEVATED / CRITICAL
│
├── Agent & alert surfaces
│   ├── /api/v1/guard     ALLOW | WARN | BLOCK · x402 v2 + v1 (EIP-3009 USDC on Base, settled before the verdict)
│   ├── /api/x402/revenue onchain ledger of every x402 payment received
│   ├── /api/mcp          Streamable HTTP MCP · 11 tools (8 read-only + 3 pager)
│   ├── /api/pager/*      watches · webhook / Telegram with /rescue deep links
│   └── /api/brief        0G Compute → Anthropic → deterministic brief
│
└── Non-custodial execution (browser, wagmi)
    ├── wallet_getCapabilities → atomic? → wallet_sendCalls (EIP-5792)
    └── fallback: sequential writeContract · every call simulateContract'd first

Mechanics

Bytecode and EIP-1967 X-Ray

Kestrel fetches runtime bytecode with eth_getCode and walks it opcode by opcode, skipping PUSH immediates and the trailing Solidity CBOR metadata. A selector is flagged only when it appears as a genuine PUSH4 operand in the dispatcher, and SELFDESTRUCT / DELEGATECALL only when they are real opcodes, not bytes inside push data. Selectors are derived with keccak256 from canonical signatures at runtime: mint, burn, pause, blacklist variants, setTax and setFees, setMaxTxAmount, upgradeTo, upgradeToAndCall.

It then reads the EIP-1967 implementation (0x3608…2bbc), admin (0xb531…6103) and beacon slots with eth_getStorageAt, plus the legacy ZeppelinOS slots that Base USDC still uses. If a proxy is found, the implementation's code is the code analyzed. EIP-1167 minimal clones resolve to their logic contract, and EIP-7702 delegated EOAs (0xef0100‖address) are recognised. Finally owner(), admin() and the admin slot are classified: renounced, single EOA, Safe (getThreshold() / getOwners() as m-of-n) or other contract.

Mechanics

Simulate the Rug: AMM math in BigInt

No reserve, sqrtPrice or allowance is ever cast to a JavaScript Number before decimal scaling.

The “Top whale dumps 100%” scenario uses a real wallet: the largest current holder among addresses active in the token's Transfer logs over the last 8 hours, excluding pools, bridge adapters and escrows.

constant product · concentrated liquidityBigInt only
Constant product (Aerodrome volatile, live fee from PoolFactory.getFee):  out = (in · (10000 − feeBps) · R_out) / (R_in · 10000 + in · (10000 − feeBps)) Shock order:  1. LP pull:     R_x ← R_x · (1 − pull),  R_y ← R_y · (1 − pull)  2. Whale sells: R_weth −= quote(whaleAmt);  R_token += whaleAmt  3. You exit:    out_after = quote(position)  drawdown = (out_before − out_after) / out_before Uniswap v3 (Robinhood Chain), from live slot0().sqrtPriceX96 and liquidity():  x_virtual = L · 2^96 / √P        y_virtual = L · √P / 2^96  exact within the active liquidity range (no tick crossings)

Mechanics

Base and Robinhood parity

$VIBES is canonical on Base (1B fixed supply). Bridging to Robinhood Chain locks tokens in the Base OFT adapter 0x4080…A381 ↗ and releases them from the Robinhood reserve adapter 0x37F2…0685 ↗. Kestrel reads both chains at their latest blocks and reports a signed verdict: EXACT when locked equals circulating, IN FLIGHT or OVER BACKED when holders are covered, UNBACKED only when circulating exceeds locked on two fresh reads.

It then compares VIBES per WETH on the Base Aerodrome pool 0x07F7…Ac45 ↗ and the Robinhood Uniswap v3 1% pool 0x3A9B…bAc5 ↗, reports the spread in basis points, and subtracts both pool fees and live gas.

The OFT rate limit is an inbound credit budget enforced on delivery, not on send. On 2026-09-05 four Base to Robinhood packets hit RateLimitExceeded at 10M per hour and were delivered between 4 minutes and 3.6 hours late, which is why the Rug Lab models a saturated bridge.

1:1 proof-of-reserve invariantboth chains at latest block
VIBES_base.balanceOf(BaseAdapter) == VIBES_rh.totalSupply() − VIBES_rh.balanceOf(RHReserveAdapter)

Mechanics

Overnight deltas

“While you slept” means the last 8 hours, resolved to a block height from each chain's block timestamps. On Base, Kestrel re-reads pool state at that historical block. Robinhood Chain's public RPC prunes historical state after roughly ten thousand blocks, so there Kestrel reconstructs the night from retained Swap, Mint and Burn event logs instead, and says so in the UI.

Mechanics

Vibestarter Backer Shield

Kestrel enumerates escrows from VibesTranchEscrowFactory.getAllEscrows() (0x36C7…Bf86 ↗), the Genesis escrow and every campaign in Vibestarter's public API on both chains. For each clone it reads getCampaign(), getActiveChallenge(), the escrow's own oracle time and, per tranche, trancheClaimed, trancheRequestedAt, trancheChallenged, getTrancheAmount and getTrancheUnlockTime, all with the deployed uint8 signatures. A tranche requested less than CHALLENGE_WINDOW (72h) ago gets a live countdown. The same reads power the ecosystem board.

Mechanics

EIP-5792 atomic rescue

At 3am mid-exploit, signing eight separate approval popups fails halfway. The Panic Bunker turns the whole rescue into one procedure.

  1. 01

    Build the bundle

    ERC-20 approve(spender, 0) revokes, one Permit2.lockdown() for every Permit2 sub-allowance, and optional unwinds through the Aerodrome router to USDC with a live getAmountsOut quote and 3% maximum slippage.
  2. 02

    Simulate every call

    Each call runs through publicClient.simulateContract. Swaps that depend on an approve in the same bundle are verified with sequential eth_simulateV1. Signing stays disabled until every call passes, and reverts are decoded into plain English.
  3. 03

    Ask the wallet what it can do

    wallet_getCapabilities reports whether the connected wallet (Coinbase Smart Wallet, Safe, Ambire) supports atomic batches.
  4. 04

    Sign once, or step through

    Atomic wallets receive one wallet_sendCalls bundle and one signature. EOAs such as MetaMask and Rabby step through the same calls with live Basescan links. Kestrel never requests or stores private keys.