Documentation
Setup, configuration and capabilities.
Everything an operator needs to run RagnaLens and everything a reader needs to interpret a report. The plain-language version lives on the methodology page.
Local setup
npm install
cp .env.example .env.local # optional: dedicated provider URLs, site URL, social links
npm run dev # http://localhost:12700
npm run test # deterministic evaluation, replay, stale-request tests
npm run typecheck && npm run buildEnvironment variables
| Variable | Purpose |
|---|---|
RAGNALENS_RPC_URL_MAINNET | Server-only. Dedicated JSON-RPC endpoint for chain 4663 (for example an Alchemy URL). Defaults to the documented public endpoint. Never sent to the browser. |
RAGNALENS_RPC_URL_TESTNET | Server-only. Same for chain 46630. |
RAGNALENS_RATE_LIMIT_PER_MINUTE | Scans per minute per client IP (default 12). In-memory; put a real limiter in front for production. |
NEXT_PUBLIC_DEFAULT_CHAIN_ID | Default network in selectors (default 4663). Must be enabled. |
NEXT_PUBLIC_ENABLED_CHAIN_IDS | Comma-separated list of enabled networks (default: both). Disabled networks show “Network disabled”. |
NEXT_PUBLIC_SITE_URL | Canonical site URL for metadata and share links. |
NEXT_PUBLIC_X_URL, NEXT_PUBLIC_GITHUB_URL | Optional social links. Omitted links are not rendered. |
NEXT_PUBLIC_TOKEN_ADDRESS, NEXT_PUBLIC_TOKEN_DEX_URL | $RAGNA contract address (blank = “CA: Soon”) and optional trading link. Publish with npm run token:ca -- 0x…, which verifies bytecode and ERC-20 metadata on chain 4663 before updating Vercel and redeploying. |
RAGNALENS_REFERENCE_TARGET, RAGNALENS_REFERENCE_CHAIN_ID, RAGNALENS_REFERENCE_LABEL | Server-only. The contract that /api/reference scans (cached ten minutes) to populate the landing page with a real report. Default: the wrapped-native token on chain 4663. |
Verified network configuration
| Network | Chain id | Currency | Explorer | Sourcify |
|---|---|---|---|---|
| Robinhood Chain | 4663 | ETH | robinhoodchain.blockscout.com | supported |
| Robinhood Chain Testnet | 46630 | ETH | explorer.testnet.chain.robinhood.com | supported |
Sources checked on 2026-09-15: Robinhood Chain developer docs — Connecting; viem chain definitions (robinhood, robinhoodTestnet). Public RPC endpoints: https://rpc.mainnet.chain.robinhood.com and https://rpc.testnet.chain.robinhood.com. Robinhood Chain is an Arbitrum-stack Layer 2 on Ethereum; ETH is the gas token.
Landing page data
Nothing on the landing page is sample data. GET /api/reference runs a real scan of the reference contract through the same pipeline as the inspector, reuses it for ten minutes, and returns the last successful report marked stale if the provider fails afterwards. The hero labels, the walkthrough card and the evidence desk read from it and say “unavailable” when it is. The machine geometry is a visual metaphor and is labelled as such.
Provider capabilities
- Reads go through a server-side relay that only forwards an allow-list of read methods. Hosts under
robinhood.comare resolved via DNS-over-HTTPS because some ISPs hijack them; TLS still validates against the real hostname. - The public endpoints throttle bursts. The relay retries 429s three times with backoff, then reports rate limiting honestly.
- The public nodes accept EIP-1898 block-hash objects with
requireCanonical(verified 2026-09-15), so scans pin by hash when the probe succeeds and fall back to number-pinning with an end-of-scan hash check. - Each scan verifies that the endpoint reports the configured chain id before reading anything.
Supported observations
| Observation | Description | Status |
|---|---|---|
| derived.address.target | Address syntax validation and checksummed form. | Implemented |
| rpc.eth_getCode.target | Runtime bytecode at the recorded block, with size and keccak256. | Implemented |
| rpc.eth_getBalance.target | Native balance at the recorded block (ETH, 18 decimals). | Implemented |
| rpc.eth_call.name / symbol / decimals / totalSupply | Token metadata calls; bytes32-style names are decoded too. | Implemented |
| rpc.eth_call.supportsInterface.* | ERC-165 probes for ERC-165, ERC-721 and ERC-1155. | Implemented |
| rpc.eth_getStorageAt.* | EIP-1967 implementation/beacon/admin, EIP-1822 proxiable and legacy OpenZeppelin slots. | Implemented |
| rpc.eth_call.beacon.implementation | implementation() on a beacon when the beacon slot is set. | Implemented |
| rpc.eth_getCode.implementation | Bytecode at the resolved implementation. | Implemented |
| derived.eip1167.target | EIP-1167 minimal proxy pattern match. | Implemented |
| rpc.eth_call.owner / getOwner / paused | Ownership and pause getters. | Implemented |
| derived.selectors.effective | PUSH4 selector scan over the effective bytecode (implementation for proxies). | Implemented |
| explorer.sourcify.target | Sourcify v2 verification lookup (unpinned; own retrieval time). | Implemented |
| Blockscout verification / holders / transfers | Blockscout answers a browser challenge to server-side callers on these networks. | Unsupported |
| Holder distribution, liquidity, transaction history, audits | No supported data source is wired in; never inferred. | Unsupported |
Rule definitions (v1.0.0)
| Id | Title | Requires |
|---|---|---|
| RL-001 | Address syntax The target is a syntactically valid 20-byte EVM address. It is normalized and, when mixed-case, its checksum must match. | derived.address.target |
| RL-002 | Runtime bytecode Reads the code at the target for the recorded block. No code means the address is not a contract at that block; it says nothing about intent. | rpc.eth_getCode.target |
| RL-003 | Native balance Records the native currency balance at the recorded block. Informational only. | rpc.eth_getBalance.target |
| RL-004 | Token interface Attempts name, symbol, decimals and totalSupply, plus ERC-165 interface declarations. A responding call is evidence of an interface, not proof of standard compliance. | rpc.eth_call.name, rpc.eth_call.symbol, rpc.eth_call.decimals, rpc.eth_call.totalSupply |
| RL-005 | Proxy pattern Reads the standardized EIP-1967, EIP-1822 and legacy OpenZeppelin proxy slots and checks for the EIP-1167 minimal proxy runtime. Unrecognized patterns are not ruled out. | rpc.eth_getStorageAt.eip1967.implementation, rpc.eth_getStorageAt.eip1967.beacon, rpc.eth_getStorageAt.eip1967.admin, rpc.eth_getStorageAt.eip1822.proxiable, rpc.eth_getStorageAt.zeppelinos.implementation, derived.eip1167.target |
| RL-006 | Ownership getter Attempts owner() and getOwner(). A set owner is a privileged party; a zero owner does not prove every privileged role is gone; a failed call proves nothing about administration. | rpc.eth_call.owner, rpc.eth_call.getOwner |
| RL-007 | Pause capability Reads paused() and looks for pause()/unpause() selectors. A pausable contract can have its transfers halted by a privileged role. | rpc.eth_call.paused, derived.selectors.effective |
| RL-008 | Privileged selectors Scans the runtime bytecode (and the resolved implementation, for proxies) for known privileged function selectors such as mint, blacklist and upgradeTo. Heuristic: selectors can be missed or misread. | derived.selectors.effective |
| RL-009 | Source verification Asks Sourcify whether verified source exists for the target. The answer is recorded with its own retrieval time and is not pinned to the block. | explorer.sourcify.target |
Inspected proxy slots: eip1967.implementation, eip1967.beacon, eip1967.admin, eip1822.proxiable, zeppelinos.implementation. Tracked selectors: 23 (15 privileged).
Deterministic replay
The evaluator is a pure function of (chainId, target, block, rulesetVersion, observations). Observations are normalized (hex lower-cased, sorted by id, duplicates rejected) before evaluation and hashing, so read order cannot change the result. The evidence id is sha256 over the canonical JSON (sorted keys, compact) of that input. Import a report on the inspector to validate it, recompute the id and re-run the rules locally.
Known limitations
- Blockscout for these networks challenges server-side callers, so explorer verification comes from Sourcify only. “Verification status unavailable” is shown when Sourcify cannot be reached.
- The rate limiter is per process. On serverless platforms each instance has its own bucket.
- Reports are cached in memory per exact (chain, target, block, rule version) only when every read completed; a new scan always pins a new block.
- Share links start a new scan. There is no server-side snapshot store; use the JSON export to keep a specific block’s report.
- Selector scanning and proxy slot inspection are heuristics with documented false negatives.
Deployment
npm run build && npm run start # Node server on :12700
# or on Vercel: import the repo, set the RAGNALENS_* / NEXT_PUBLIC_* variables, deploy.
# API routes run on the Node runtime; /api/inspect streams NDJSON and allows up to 60 s.Security notes
- All request inputs are validated server-side with Zod; addresses are re-parsed and checksummed.
- Only the two allow-listed endpoints are ever contacted; user input can never choose an RPC URL.
- No wallet connection, signatures, approvals or transactions exist anywhere in the product.
- Token metadata and explorer content are sanitized, length-bounded and rendered as plain text only.
- Imported reports are schema-validated and size-bounded before anything is rendered.