Autonomous agent that watches Ethereum proxy upgrades in real time and flags risky changes within seconds.
Proxy upgrades are one of the most exploited surfaces in DeFi. Auditors are slow, expensive, and asleep at 3 AM. Vigil closes the gap between "an upgrade lands on chain" and "someone qualified looks at it."
Vigil watches every block on Base mainnet (chainId 8453) for EIP-1967 Upgraded(address) events, Base Sepolia (84532) is supported as the dev / demo environment. When one fires, the pipeline runs:
- Detect,
upgradeWatcherreads the new implementation from the event and the old implementation from the EIP-1967 storage slot atblock - 1. - Verify,
sourcifychecks Sourcify (with retries), and if unverified, runs a bytecode similarity search to flag possible clones of known contracts. - Diff, storage layouts (
diffStorage) and ABIs (diffFunctions) are compared. Moved/removed slots and added/removed/modified functions become risk flags. Sensitive names (upgradeTo,withdraw,selfdestruct, …) bump severity. - Score, combines storage and function risk into
LOW | MEDIUM | HIGH | CRITICAL. Partial Sourcify matches bump severity by one. - Analyse,
agent/analysersends the diff bundle (plus NatSpec when available) to OpenAIgpt-4oand returns a structured{summary, explanation, recommendation, confidence}JSON. - Publish: the alert is logged, appended to
data/alerts.json(per-chain, so Base mainnet alerts and Base Sepolia alerts route to separate stores), and uploaded to Swarm via@ethersphere/bee-jsthrough thebzz.limogateway (no postage stamp required). The{alert, block}envelope is added to a single Mantaray manifest underblocks/<n>; the manifest's signed feed (topicvigil-manifest) is the canonical subscriber endpoint and persists across restarts whenSWARM_PRIVATE_KEYis pinned. After each alert, the agent also writes four reputation text records (vigil.last-severity,vigil.last-upgrade-at,vigil.last-tx,vigil.upgrade-count) back to the matching subname onvigilbot.eth(Ethereum mainnet), turning ENS into a live, on-chain reputation log other agents can read.
The frontend pulls alerts from (in order) the configured Swarm feed URL, local data/alerts.json for Base mainnet, data/alerts-base-sepolia.json (or an inline TS seed) for Base Sepolia, falling back to mock data when nothing is available. A chain selector switches the view between Base and Base Sepolia, and any alert can be opened in a per-alert AI chat grounded in that alert's JSON. The dashboard's header card resolves agent.vigilbot.eth live from Ethereum mainnet ENS (description, capabilities, severity floor, vigil.feed URL); each alert card embeds a reputation panel resolved from the matching target subname.
When the demo wallet (or a wallet the user has connected) has a non-zero approval on the same proxy that just emitted an upgrade alert, the alert card surfaces a "Your wallet is exposed, revoke approval" banner. One click submits approve(spender, 0). If the user has an injected wallet connected, wagmi routes the tx through it (MetaMask popup, chain-switch prompt if needed); otherwise the frontend falls back to signing directly via viem with the embedded testnet NEXT_PUBLIC_DEMO_WALLET_PRIVATE_KEY so the demo can run hands-off.
src/ backend agent (TypeScript, Node.js, ESM)
├── agent/ pipeline entrypoint + AI analyser
├── watchers/ EIP-1967 Upgraded event listener
├── sourcify/ verification, storage/ABI diffs, similarity
├── alerts/ Alert type + severity + console rendering
├── delivery/ logAlert + atomic JSON store (data/alerts.json)
├── swarm/ bee-js Mantaray manifest publisher (blocks/<n>)
└── ens/ Ethereum Sepolia ENS reader + reputation writer
for vigil.eth and its subnames
scripts/ one-shot operational scripts
├── ens/ check-parent, register-parent, seed-subnames,
│ set-base-primary, sync-feed, resolve
└── swarm/ seed-feed (bootstrap alert so vigil.feed resolves
before the first real upgrade)
frontend/ Next.js 16 dashboard (React 19, Tailwind v4)
├── app/page.tsx thin entry, fetches alerts + agent identity +
│ target reputations server-side, renders <Dashboard />
├── app/components/ Dashboard, AlertList (with inline EnsReputationPanel),
│ AgentIdentityCard, ChatPanel, UpgradesChart,
│ ChainSelector, ConnectButton, RevokeBanner,
│ ViewChainContext, Web3Provider, CopyButton
├── app/api/chat/route.ts streaming AI chat grounded on a single alert
└── lib/ types, alert loader (Swarm + JSON + seed),
wallet config (wagmi/viem), approval hooks,
ENS resolver (viem, server-side),
formatters, mock + seed alerts
demo-target/ Hardhat sub-project, intentionally-vulnerable
UUPS proxy on Base Sepolia for driving demos
├── contracts/ DemoTokenV1.sol + DemoTokenV2.sol (3 deliberate sins)
├── scripts/ deploy-proxy, trigger-upgrade, reset,
│ seed-demo-wallet (mint + approve for revoke flow)
├── deployments/ per-network JSON written by deploy-proxy (gitignored)
└── DEPLOYMENTS.md live test addresses + replay instructions
data/alerts.json Base mainnet alert store (gitignored)
data/alerts-base-sepolia.json Base Sepolia alert store (gitignored)
docs/ development plan, pitch, swarm notes,
specs, plans, ideas, mentor feedback
- TypeScript (ESM, NodeNext) on Node.js, run with
tsx - ethers v6 for RPC + event subscription
- Sourcify v2 for verified sources, storage layouts, ABIs, NatSpec, similarity
- OpenAI
gpt-4ofor risk analysis (Groq SDK is installed and stubbed as a fallback inagent/analyser, but currently commented out) @ethersphere/bee-jssigned Swarm feed publishing through thebzz.limogateway withNULL_STAMP(no postage batch needed)- ENS reader + writer (ethers) for
agent.vigilbot.ethidentity records,demo.vigilbot.ethENSIP-11 multichainaddr[base-mainnet], an address→name JSON cache, and per-target reputation text records written after every alert (VIGIL_ENS_NETWORK=sepoliaswitches the whole stack to the legacyvigil.ethparent for local testing)
- Next.js 16 + React 19 + Tailwind CSS v4
- wagmi v2 + viem +
@tanstack/react-queryfor wallet connection, allowance reads, andapprove(0)writes - Vercel AI SDK (
ai,@ai-sdk/react,@ai-sdk/openai) for streaming chat - Recharts for the 24-hour upgrade timeline
- Sourcify, v2 API for contract verification, storage layout diffing, ABI comparison, NatSpec extraction, and bytecode similarity search across 27M+ verified contracts
- Apify, autonomous social context enrichment via X402 micropayments on Base. When an upgrade fires, Vigil pays $1 USDC autonomously to scrape news context, no human in the loop
- Swarm, permanent decentralised storage of every alert and block via Mantaray manifest, indexed by block number and accessible via bzz.limo. Permanent feed URL never changes
- ENS, agent identity, capability discovery, and per-protocol reputation anchored to vigilbot.eth on Ethereum mainnet
- Telegram, real-time alert delivery with AI summary, Swarm link, and Basescan link
Backend:
npm install
cp .env.example .env # then fill in keys (see below)
npm run dev # starts the watcher + pipelineFrontend (in a second terminal):
cd frontend
npm install
cp .env.local.example .env.local # NEXT_PUBLIC_DEMO_* + optional SWARM_FEED_URL
npm run dev # http://localhost:3000The dashboard pulls alerts from (in priority order) the configured Swarm feed URL, the local data/alerts.json for Base mainnet, the local data/alerts-base-sepolia.json (or an inline TS seed) for Base Sepolia, falling back to mock data when nothing is available. The chain selector toggles which network's alerts are shown. The "live"/"mock" pill in the header reflects the source.
Vigil ships as two Railway services sharing one project / environment:
| Service | Root | Public | Start | Branch |
|---|---|---|---|---|
Worker |
/ (repo root) |
unexposed (no port, no healthcheck) | npm start → tsx src/agent/index.ts |
dev |
Web |
frontend/ |
port 8080 (public domain) |
npm start → next start |
dev |
Railway's Nixpacks/Railpack auto-detects Node and runs npm ci for both. No Dockerfile or railway.json needed.
Define these once at the environment level and reference them from each service via ${{shared.VAR}}:
| Variable | Worker | Web |
|---|---|---|
RPC_URL |
✅ | , |
POLL_INTERVAL_MS |
✅ | , |
OPENAI_API_KEY |
✅ | ✅ |
GROQ_API_KEY |
✅ | , |
SWARM_PRIVATE_KEY |
✅ | , |
SWARM_FEED_URL |
, | ✅ |
NEXT_PUBLIC_DEMO_WALLET |
, | ✅ |
NEXT_PUBLIC_DEMO_WALLET_PRIVATE_KEY |
, | ✅ |
NEXT_PUBLIC_DEMO_SPENDER |
, | ✅ |
NEXT_PUBLIC_DEMO_PROXY_BASE_SEPOLIA |
, | ✅ |
NEXT_PUBLIC_DEMO_PROXY_BASE |
, | ✅ |
The Worker generates its keypair on first boot if SWARM_PRIVATE_KEY is unset. To get a stable feed URL the frontend can point to:
-
Deploy the Worker without
SWARM_PRIVATE_KEY. On startup it prints the generated key + owner address + manifest topic (vigil-manifest) hex to Deploy Logs. -
Copy the printed key into the shared var
SWARM_PRIVATE_KEY. Without this, every restart spawns a fresh owner and subscribers fetch from a stale URL. -
Build the feed URL, owner from the logs, topic is the deterministic hex of
vigil-manifest(also printed on every boot):https://bzz.limo/feeds/<ownerAddress>/<topicHex> -
Set that as
SWARM_FEED_URLon the Web service, or publish it to ENS and let the frontend (and any third-party subscriber) discover it vianpm run ens:sync-feedfollowed bynpm run ens:resolve agent.vigil.eth. The agent verifies on boot thatvigil.feedon ENS matches its actual feed URL and warns loudly when it doesn't.
- Service Type / Networking: unexposed (no public domain, no healthcheck). The agent is a long-running poller, not an HTTP server, Railway will mark it failed if a healthcheck is enabled.
- Branch:
dev,mainhistorically lacked thenpm startscript so the container would crash withCannot find module '/app/index.js'. - Logs: the Deploy Logs tab streams the agent's stdout (
[Vigil] Connected to network,[UpgradeWatcher] Listening,[Swarm] Alert published, etc.). The Network Flow Logs tab is low-level packet captures and not useful for app-level debugging.
Vigil's agent and the contracts it watches have human names anchored to vigilbot.eth, registered on Ethereum mainnet (chain id 1). Subnames carry the agent's runtime configuration and point to the addresses the agent actually watches on Base mainnet (chain id 8453) via ENSIP-11 multichain addr records.
A Sepolia code path (vigil.eth parent, Base Sepolia subnames) is preserved for local testing, opt in with VIGIL_ENS_NETWORK=sepolia on both the Worker and the Web service. The active network is selected at runtime by that env var; both default to mainnet. The record schema is identical on both networks.
| Subname | Purpose | Records |
|---|---|---|
agent.vigil.eth |
Agent identity. Read at boot. | description, url, vigil.capabilities (JSON), vigil.severity-min, vigil.feed (Swarm subscriber URL), vigil.payment |
demo.vigil.eth |
Demo proxy that gets live-upgraded during the pitch. | description, vigil.kind=demo-proxy, addr[base-sepolia] (ENSIP-11), and reputation records (see below) |
<protocol>.vigil.eth |
Real watched protocols. | same shape as demo.vigil.eth |
Three ENS mechanisms do the work:
- ENSIP-11 multichain address records, Sepolia subnames carry
addrrecords typed for the Base Sepolia coin type (0x80000000 | 84532 = 2147567180), so the frontend resolves a name on Sepolia and renders Base Sepolia state. The revoke flow uses this:RevokeBannerreads the demo proxy address fromdemo.vigil.eth's ENSIP-11 record, falling back toNEXT_PUBLIC_DEMO_PROXY_BASE_SEPOLIAonly when ENS resolution is unavailable. - ENSIP-19 L2 reverse names,
demo.vigil.ethis set as the primary name for the demo proxy on Base Sepolia, so explorers display the human name instead of0xab…cd. - Reputation text records: after every alert, the agent writes
vigil.last-severity,vigil.last-upgrade-at,vigil.last-tx, andvigil.upgrade-count(incremented) to the matching target subname. ENS becomes a live, on-chain reputation log:npm run ens:resolve usdc.vigil.ethshows whether Vigil has flagged the protocol recently and how often.
The agent reads agent.vigil.eth's text records at boot, logs the resolved capabilities, and verifies an invariant: the vigil.feed URL it advertises on ENS must match the Swarm feed URL it actually publishes to. A mismatch (e.g. SWARM_PRIVATE_KEY rotated without re-running npm run ens:sync-feed) triggers a loud warning so subscribers don't fetch from a stale URL. If ETH_SEPOLIA_RPC_URL is unset, the agent gracefully degrades to legacy mode (no ENS identity, no name tagging, no reputation writes). Address-to-name lookups for tagging alerts use a small JSON cache at data/ens-targets.json populated by the seed script.
Other agents and frontends can find Vigil's feed entirely through ENS, no out-of-band URL sharing:
# 1. Resolve the agent's ENS name to its Swarm feed URL
npm run ens:resolve agent.vigil.eth # prints vigil.feed: https://bzz.limo/feeds/<owner>/<topic>
# 2. Fetch alerts from the feed (latest)
curl <vigil.feed URL>Reputation lookup is symmetric, npm run ens:resolve demo.vigil.eth returns the latest severity, last upgrade timestamp, last tx hash, and total upgrade count Vigil has observed for that target.
Production, Ethereum mainnet + Base mainnet (vigilbot.eth):
| Record | Chain · address / tx | Notes |
|---|---|---|
vigilbot.eth (registered) |
Ethereum mainnet | Production parent. Owner 0xf5B1d9144d9D005CD74cFC2d1A22cbAF4e8E8736. |
demo.vigilbot.eth proxy |
Base mainnet · 0x91F276F9…6358f |
UUPS proxy used for the live revoke demo. Latest impl + tx history in demo-target/DEPLOYMENTS.md. |
| ENSIP-19 reverse on Base mainnet | Base mainnet · 0xe8ece43a…ab6561, block 45784174 |
Demo proxy → primary name demo.vigilbot.eth. L2ReverseRegistrar 0x0000000000D8e504002cC26E3Ec46D81971C1664. |
Dev / legacy, Ethereum Sepolia + Base Sepolia (vigil.eth):
| Record | Chain · tx | Notes |
|---|---|---|
vigil.eth (registered + wrapped, 1 year) |
Sepolia · 0x0802bd0d…b15c4f, block 10821165 |
Owner 0xf5B1d9144d9D005CD74cFC2d1A22cbAF4e8E8736. Native NameWrapper wrap as part of register(). |
agent.vigil.eth subname + records |
Sepolia · 0xe22e4bd6…ae3b36 |
description, url, vigil.capabilities (JSON), vigil.severity-min=MEDIUM. |
demo.vigil.eth subname + records |
Sepolia · 0x0b9772fc…605f99 |
description, vigil.kind=demo-proxy, ENSIP-11 addr[base-sepolia] → 0x65953e7c7C8A0Ee61be3b33BD88E2961439B21AD. |
vigil.feed text record |
Sepolia · 0x319da24e…21cee |
agent.vigil.eth → vigil.feed → bzz.limo/feeds/0x4b291F22…/94f6a4be… for subscriber discovery. |
vigil.payment text record |
Sepolia · 0x648a93a1…fdc5601e |
agent.vigil.eth → vigil.payment → x402:https://api.vigil.app/pay/per-alert. Discoverable X402 endpoint, stacks with Apify/Umia narratives. |
| ENSIP-19 reverse on Base Sepolia | Base Sepolia · 0xe5f947d7…dc9093, block 41284414 |
Demo proxy → primary name demo.vigil.eth. setNameForOwnableWithSignature on L2ReverseRegistrar 0x00000BeEF055f7934784D6d81b6BC86665630dbA; proxy is OwnableUpgradeable so deployer wallet's ERC-191 signature authorizes, no contract change needed. |
| Smoke-test reputation write | Sepolia · 4 setText txs | First populated demo.vigil.eth reputation: vigil.last-severity=CRITICAL, vigil.upgrade-count=1. |
| Run #2 demo upgrade (live e2e replay) | Base Sepolia · 0x7ee96f68…7b2f7ead, block 41287554 |
Agent watching Base Sepolia detected Upgraded(address), ran the full pipeline, emitted CRITICAL alert tagged demo.vigil.eth, wrote reputation back (count 1→2). |
| Run #2 latest replayable upgrade | Base Sepolia · 0x20fbbcf4…6071881, block 41290914, impl 0x6608a5C1…7cB035 |
Bumping VIGIL_DEMO_BUILD in DemoTokenV2.sol lets npm run upgrade (or npm run demo-cycle) deploy a fresh impl → new Upgraded(address) event the agent picks up, without changing the proxy address. Same proxy, replayable demo. Seven prior upgrades archived in previousUpgrades. |
Verify on the ENS app: https://app.ens.domains/agent.vigil.eth?network=sepolia
If you ever need to redeploy under a different parent name or against a fresh wallet. Append --network=mainnet to any ENS script for the production parent (vigilbot.eth on mainnet); omit for Sepolia (vigil.eth). The same ENS_REGISTRAR_PRIVATE_KEY signs both, fund it on whichever network you target.
-
Register the parent. The repo ships an end-to-end script that handles commit-reveal + native NameWrapper wrap in one go. Cost ≈ 0.003 ETH/year for 5+ char names on Sepolia (mainnet pricing varies by name length and premium):
tsx scripts/ens/check-parent.ts # Sepolia (default) tsx scripts/ens/check-parent.ts --network=mainnet # mainnet preflight tsx scripts/ens/register-parent.ts --network=mainnet --name=vigilbot.eth
-
Create subnames + records:
tsx scripts/ens/seed-subnames.ts # Sepolia tsx scripts/ens/seed-subnames.ts --network=mainnet # mainnet (vigilbot.eth)
-
Publish the agent's Swarm feed URL on the active agent subname so subscribers can discover it. Re-run any time
SWARM_PRIVATE_KEYrotates:tsx scripts/ens/sync-feed.ts # Sepolia tsx scripts/ens/sync-feed.ts --network=mainnet # mainnet
-
(Optional) Bootstrap the feed with a placeholder alert so
vigil.feedreturns parseable JSON to subscribers before the first real upgrade lands:tsx scripts/swarm/seed-feed.ts
-
(Optional) ENSIP-19 reverse on Base. Note this script uses
--network=base-sepolia | base-mainnet(different from the ENS scripts). Verify the L2 reverse registrar address against https://docs.ens.domains/learn/deployments:tsx scripts/ens/set-base-primary.ts # default base-sepolia tsx scripts/ens/set-base-primary.ts --network=base-mainnet -
Sanity-check live resolution at any time:
tsx scripts/ens/resolve.ts agent.vigil.eth tsx scripts/ens/resolve.ts --network=mainnet agent.vigilbot.eth tsx scripts/ens/resolve.ts --network=mainnet demo.vigilbot.eth
The agent reads its identity, capabilities, and severity threshold from agent.vigil.eth text records at startup, and tags every alert with the human name from the ENS cache. The frontend renders demo.vigil.eth (0xab…cd) instead of bare hex on every alert that has a name. Block explorers display demo.vigil.eth via ENSIP-19 reverse resolution. Strip the ENS module out and the agent runs in a noticeably degraded "legacy" mode with bare hex everywhere.
The demo-target/ sub-project deploys an intentionally-vulnerable UUPS proxy on Base Sepolia so the agent has an upgrade event to react to on demand. V2 ships three deliberate sins (storage collision, unguarded drain, mint losing onlyOwner), each one detectable by a different signal in Vigil's pipeline.
Status: end-to-end run #1 completed live on Base Sepolia (2026-05-09). Both implementations are verified on Sourcify and Basescan; the upgrade tx fired the
Upgraded(address)event Vigil watches for. Addresses recorded indemo-target/DEPLOYMENTS.md. Design spec atdocs/superpowers/specs/2026-05-08-demo-target-design.md.
Workflow (Base Sepolia by default, append :mainnet to any script for Base mainnet):
cd demo-target
npm install
cp .env.example .env # DEPLOYER_PRIVATE_KEY + BASE_SEPOLIA_RPC_URL + BASE_MAINNET_RPC_URL
# + ETHERSCAN_API_KEY; add DEMO_WALLET_PRIVATE_KEY + DEMO_SPENDER_ADDRESS for seed-demo-wallet
npm run cycle # Sepolia: reset + deploy V1 + upgrade to V2 in one shot
npm run cycle:mainnet # Base mainnet equivalent (creates a new proxy, only run for first-time setup)
# or run the steps individually:
npm run deploy # deploy V1 proxy on Sepolia, verify on Sourcify + Basescan
npm run deploy:mainnet # same on Base mainnet
npm run seed-demo-wallet # mint 1,000 VIGIL + approve(spender, 1,000) from a separate demo wallet
npm run upgrade # deploy V2, upgrade proxy, fires Upgraded(address)
npm run upgrade:mainnet # same on Base mainnet
npm run reset # wipe Sepolia deployments/ to start from a clean slate
npm run reset:mainnet # wipe Base mainnet deployments/ETHERSCAN_API_KEY is the Etherscan v2 multichain key (one key covers both Base networks). Sourcify verification needs no key. DEMO_WALLET_PRIVATE_KEY and DEMO_SPENDER_ADDRESS are only needed when running seed-demo-wallet[:mainnet] to set up the revoke-on-upgrade demo flow.
Point Vigil's RPC_URL at the same Base Sepolia endpoint as the demo target while testing, or use the recorded run #1 addresses to replay the upgrade event without burning gas (see DEPLOYMENTS.md).
The marquee demo: a wallet with an active approval on the demo proxy gets a one-click "revoke before exploit" path the moment the malicious V2 upgrade hits.
- Run
npm run cyclefromdemo-target/to deploy V1 + V2 fresh, thennpm run seed-demo-walletto mint 1,000 VIGIL andapprove(DEMO_SPENDER, 1,000)from the demo wallet. - Paste the new proxy address into
frontend/.env.localasNEXT_PUBLIC_DEMO_PROXY_BASE_SEPOLIA, plusNEXT_PUBLIC_DEMO_WALLET,NEXT_PUBLIC_DEMO_WALLET_PRIVATE_KEY, andNEXT_PUBLIC_DEMO_SPENDER. - Open the dashboard and switch the chain selector to Base Sepolia. The dashboard reads
allowance(DEMO_WALLET, DEMO_SPENDER)on every refresh. - Trigger the upgrade (
npm run upgrade). Vigil's pipeline emits a new alert; the matching alert card surfaces a red "Your wallet is exposed, revoke approval" banner. - Click Revoke approval. With no wallet connected, the frontend signs
approve(DEMO_SPENDER, 0)with the embedded private key and submits via viem, no MetaMask popup. With a wallet connected via the Connect wallet button, wagmi routes the tx through it instead. After mining, the banner flips to green "Approval revoked. Allowance is now 0."
Implementation in RevokeBanner (UI + orchestration), approvals (allowance hook), and wallet (wagmi config + per-chain demo proxy resolution). Plan: docs/plans/2026-05-09-revoke-on-upgrade-mvp.md.
Once V1 is deployed and you've run a pitch through to Revoke approval (allowance is now 0), use demo-cycle from demo-target/ to reset for the next pitch in one shot:
cd demo-target
npm run demo-cycle # Base Sepolia (default)
npm run demo-cycle:mainnet # Base mainnet (production demo)This bumps the VIGIL_DEMO_BUILD constant in DemoTokenV2.sol (so the new impl bytecode is unique → fresh Upgraded event), deploys a fresh V2 impl, calls upgradeToAndCall on the existing proxy, verifies on Sourcify, and re-approves DEMO_WALLET → DEMO_SPENDER for 1,000 VIGIL. End-to-end ≈ 30 s, ≈ 0.0005 ETH on Base Sepolia (mainnet costs more, gas-dependent). Same proxy address across pitches, so ENS records and frontend env vars don't need re-pointing.
Inside Claude Code, the demo-cycle skill wraps the same script and additionally polls data/alerts-base-sepolia.json to confirm the watcher saw the new alert. Trigger it with phrases like "demo cycle", "arm the demo", or "prep the pitch". Spec + plan: docs/superpowers/specs/2026-05-09-demo-cycle-design.md, docs/superpowers/plans/2026-05-09-demo-cycle.md.
After running, open the UI on Base Sepolia with no wallet connected and the red "Your wallet is exposed" banner reappears on the new alert card.
Three separate .env files, one per sub-project.
Agent, root .env
Core pipeline:
| Variable | Purpose |
|---|---|
RPC_URL |
Base mainnet (or Base Sepolia for dev) RPC endpoint. |
POLL_INTERVAL_MS |
Block poll cadence for the watcher. |
OPENAI_API_KEY |
Used by the analyser (gpt-4o). |
SWARM_PRIVATE_KEY |
32-byte hex key used to sign the Swarm feed. Auto-generated and printed on first run if missing, copy it back into .env to keep the same feed across restarts. |
ENS, read at agent boot and consumed by the ENS scripts:
| Variable | Purpose |
|---|---|
VIGIL_ENS_NETWORK |
mainnet (default) or sepolia. Selects which ENS parent the agent reads at boot. mainnet reads vigilbot.eth; sepolia is preserved for local testing only. |
ETH_MAINNET_RPC_URL |
Ethereum mainnet RPC. Required for the default VIGIL_ENS_NETWORK=mainnet and by --network=mainnet script invocations (registration, seeding, reputation writes against vigilbot.eth). |
ETH_SEPOLIA_RPC_URL |
Ethereum Sepolia RPC. Only required if VIGIL_ENS_NETWORK=sepolia or by --network=sepolia script invocations. Without it, the agent runs in legacy mode (no name tagging, no severity floor). |
VIGIL_PARENT_ENS_NAME |
Defaults to vigil.eth (Sepolia parent). |
VIGIL_PARENT_ENS_NAME_MAINNET |
Defaults to vigilbot.eth (mainnet parent). |
VIGIL_AGENT_ENS_NAME |
Defaults to agent.vigil.eth (Sepolia agent subname). |
VIGIL_AGENT_ENS_NAME_MAINNET |
Defaults to agent.vigilbot.eth (mainnet agent subname). |
ENS_REGISTRAR_PRIVATE_KEY |
Required by all ENS write scripts (register-parent, seed-subnames, set-base-primary, sync-feed). Also enables the agent's per-alert reputation writes. Without it, the agent runs read-only. The same key signs both Sepolia and mainnet writes; the wallet must own the parent name on the target network. |
BASE_SEPOLIA_RPC_URL |
Used by set-base-primary --network=base-sepolia. Defaults to https://sepolia.base.org. |
BASE_SEPOLIA_PRIVATE_KEY |
Used by set-base-primary --network=base-sepolia to sign the ERC-191 message authorising the L2ReverseRegistrar. Must be the deployer (owner) of the demo proxy. |
BASE_SEPOLIA_REVERSE_REGISTRAR |
Defaults to 0x00000BeEF055f7934784D6d81b6BC86665630dbA. Override only if ENS deploys a new address. |
BASE_MAINNET_RPC_URL |
Used by set-base-primary --network=base-mainnet and the chain-archive backfill utility. |
BASE_MAINNET_PRIVATE_KEY |
Used by set-base-primary --network=base-mainnet. Falls back to ENS_REGISTRAR_PRIVATE_KEY or DEPLOYER_PRIVATE_KEY if unset. |
The committed
.env.examplestill listsGROQ_API_KEY,APIFY_API_KEY,SPACECOMPUTER_API_KEY,SWARM_BEE_URL, andSWARM_POSTAGE_STAMP. None of those are read by current code, Swarm now publishes tobzz.limowithNULL_STAMP. The example file is overdue for a clean-up.
Frontend, frontend/.env.local
| Variable | Purpose |
|---|---|
SWARM_FEED_URL |
If set, the dashboard fetches the latest alert from this Swarm feed URL on load. Empty disables Swarm and falls back to local files / seed / mock. |
VIGIL_ENS_NETWORK |
mainnet (default) or sepolia. Picks which ENS parent the dashboard resolves identity + reputation against, mainnet reads vigilbot.eth, sepolia is preserved for local testing only. |
ETH_MAINNET_RPC_URL |
Server-side only. Required for the default VIGIL_ENS_NETWORK=mainnet. Powers the live ENS lookups for the agent identity card and per-alert reputation panels against vigilbot.eth. |
ETH_SEPOLIA_RPC_URL |
Server-side only. Only required if VIGIL_ENS_NETWORK=sepolia. Without an RPC for the active network, the panels degrade gracefully and the dashboard falls back to NEXT_PUBLIC_DEMO_PROXY_BASE_SEPOLIA for the revoke flow. |
OPENAI_API_KEY |
Powers the per-alert chat panel (gpt-4o-mini) via the Vercel AI SDK. |
NEXT_PUBLIC_DEMO_WALLET |
Address whose allowance the dashboard reads to decide whether to show the revoke banner. |
NEXT_PUBLIC_DEMO_WALLET_PRIVATE_KEY |
Testnet demo only. Embedded in the frontend bundle so the revoke button can sign approve(spender, 0) without a wallet popup. Anyone with devtools can read it, use a dedicated key that holds nothing else. |
NEXT_PUBLIC_DEMO_SPENDER |
Address pre-approved on the demo proxy, what the revoke button passes to approve(_, 0). |
NEXT_PUBLIC_DEMO_PROXY_BASE_SEPOLIA |
Fallback demo proxy address on Base Sepolia. The revoke flow first tries to resolve demo.vigil.eth's ENSIP-11 addr[base-sepolia] record and only falls back to this when ENS is unreachable. |
NEXT_PUBLIC_DEMO_PROXY_BASE |
Demo proxy address on Base mainnet, intentionally empty until/unless we deploy there. |
Demo target, demo-target/.env
| Variable | Purpose |
|---|---|
BASE_SEPOLIA_RPC_URL |
Base Sepolia RPC. Used by every npm run * script without the :mainnet suffix. |
BASE_MAINNET_RPC_URL |
Base mainnet RPC. Used by every npm run *:mainnet variant (deploy:mainnet, upgrade:mainnet, demo-cycle:mainnet, cycle:mainnet). |
DEPLOYER_PRIVATE_KEY |
Funded EOA used to deploy V1, V2, and the proxy. The same key works on both networks; fund it on whichever you target. |
ETHERSCAN_API_KEY |
Etherscan v2 multichain key, one key covers Base Sepolia and Base mainnet (Sourcify needs no key). |
DEMO_WALLET_PRIVATE_KEY |
Optional, only required by seed-demo-wallet[:mainnet]. Funded EOA whose approval the revoke flow targets. |
DEMO_SPENDER_ADDRESS |
Optional, only required by seed-demo-wallet[:mainnet]. Address that gets pre-approved for 1,000 VIGIL by the demo wallet. |
Early WIP. Built for ETHPrague 2026.
What works today:
- Live block-by-block watching of EIP-1967
Upgraded(address)events - Sourcify verification (with retries) and bytecode similarity fallback for unverified implementations
- Storage-layout and ABI diffing with severity scoring + sensitive-name flagging + partial-match bump
- OpenAI
gpt-4oanalyser producing structured risk JSON - Atomic JSON stores per chain (
data/alerts.json,data/alerts-base-sepolia.json) with deduplication bytxHashandchainIdtagging - Single Mantaray manifest on Swarm holding
blocks/<n>paths, persisted across restarts via a signed feed (topicvigil-manifest,NULL_STAMP,bzz.limogateway, no postage batch needed) - Next.js dashboard with alert list, 24h upgrades chart, severity stats, copy-to-clipboard, per-alert AI chat, Base/Base-Sepolia chain selector, and Swarm-feed-URL alert source (cream/navy theme; logo at
frontend/public/vigil-logo.png) - Revoke-on-upgrade banner with dual signing: routes through a connected injected wallet via wagmi when one is present, or falls back to a viem
walletClientsigning with the embedded testnetNEXT_PUBLIC_DEMO_WALLET_PRIVATE_KEYfor hands-off demo runs - Optional Connect wallet button (wagmi
injected()connector): the dashboard accepts an external wallet for the revoke flow and shows the demo wallet pill until one is connected - ENS identity + reputation anchored to
vigilbot.ethon Ethereum mainnet (the legacyvigil.ethSepolia parent is preserved as a code path for local testing viaVIGIL_ENS_NETWORK=sepolia). Agent readsagent.vigilbot.ethat boot for capabilities, severity floor, andvigil.feedURL invariant; tags every alert with the resolved human name from a JSON cache; writes 4 reputation text records (vigil.last-severity,vigil.last-upgrade-at,vigil.last-tx,vigil.upgrade-count) back to the matching target subname after each alert; and publishes the Swarm subscriber URL on ENS for discovery. ENS lifecycle scripts (check-parent,register-parent,seed-subnames,set-base-primary,sync-feed,resolve) all take--network=sepolia|mainnet; Base reverse-naming uses--network=base-sepolia|base-mainnet. Live deployment recorded in the ENS section. - Dashboard ENS surfacing: header card resolves
agent.vigil.ethlive from Sepolia ENS (description, capabilities chips, severity-floor pill,vigil.feedURL); each alert card embeds anEnsReputationPanelshowinglast-severity,upgrade-count, andlast-upgrade-atfor the matching subname. Both fetched server-side via viem infrontend/lib/ens.ts. - ENSIP-11 demo proxy resolution:
RevokeBannerreads the demo proxy address fromdemo.vigil.eth'saddr[base-sepolia]record at request time;NEXT_PUBLIC_DEMO_PROXY_BASE_SEPOLIAis now a fallback for environments withoutETH_SEPOLIA_RPC_URL. demo-target/UUPS proxy with V1/V2 contracts,deploy/upgrade/reset/cycle/seed-demo-walletscripts, dual Sourcify + Basescan verification, live-tested on Base Sepolia (run #1 addresses indemo-target/DEPLOYMENTS.md)
In progress / not yet wired:
- LangChain (in
package.jsonbut no imports insrc/) - Root
.env.exampleis overdue for a clean-up,GROQ_API_KEY,APIFY_API_KEY,SPACECOMPUTER_API_KEY,SWARM_BEE_URL, andSWARM_POSTAGE_STAMPare listed but not consumed by current code - Base mainnet demo proxy (
NEXT_PUBLIC_DEMO_PROXY_BASE) intentionally empty, revoke flow is Sepolia-only by design
See LICENSE.