Metadata-Version: 2.4
Name: alchem-link
Version: 0.23.0
Summary: Alchemy x Chainlink developer toolkit — oracle consumer-safety auditing, guard simulation, measured feed cadence, cross-chain divergence, and a full terminal UI, with zero dependencies
Author: meta-oracle
License-Expression: MIT
Project-URL: Homepage, https://github.com/Meta-Oracle/Scematica
Project-URL: Repository, https://github.com/Meta-Oracle/Scematica
Project-URL: Issues, https://github.com/Meta-Oracle/Scematica/issues
Keywords: alchemy,chainlink,web3,oracle,rpc,defi,ethereum,price-feed,keccak,multicall,security,tui,terminal,prometheus,simulation
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Security
Classifier: Topic :: Terminals
Classifier: Environment :: Console
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# Alchem-Link v0.23.0

An Alchemy × Chainlink developer toolkit that reads chains instead of documentation.
Live oracle reads, a consumer-safety audit, measured feed behaviour, and a simulator that
replays your guards against the failure modes that have already cost people money — from
the command line, a full-screen dashboard, or Python, with **no dependencies at all**.

Not "no dependencies except the UI". The terminal system is in the package: screen
diffing, colour-depth negotiation, input parsing, widgets and the event loop all live in
`alchem_link.term`. `pip install alchem-link` pulls nothing, and that now includes the
dashboard.

```bash
pip install alchem-link
alchem-link price ETH/USD
```

```
ETH / USD  (ethereum)
  price      1,929.60
  status     FRESH
  updated    18m 7s ago (heartbeat 1h)
  round      129127208515966893596  (phase 7)
  aggregator 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419
```

No API key needed to start. Set `ALCHEMY_API_KEY` when you want real rate limits.

## The problem this exists for

`latestRoundData()` succeeding tells you almost nothing. It succeeds when the feed has
not published in a day, when the answer is pinned against a circuit breaker, when the
round is a carried-over duplicate, and — on an L2 — when the sequencer has been down for
an hour and the price is a fossil. **Every one of those returns a well-formed number and
raises nothing.** Each has cost real protocols real money.

`alchem-link audit` runs the checks a careful consumer contract would:

```bash
alchem-link audit ETH/USD -n base
```

```
ETH / USD  (base)  1,930.24   [ok]
  0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70
  [info] BOUNDS_WIDE: circuit breakers are far from the current price
         the price would have to fall 1.93e+11x to reach minAnswer
  [info] SEQUENCER_OK: L2 sequencer is up and past its grace period
         up for 3619518s, past the 3600s grace period
```

| Check | Catches |
|---|---|
| `STALE` | Fresh-looking answer that has not published within its heartbeat |
| `NON_POSITIVE` | Zero or negative answers, which are never real quotes |
| `INCOMPLETE_ROUND` | `updatedAt == 0` — a round started but never finalised |
| `CARRIED_ROUND` | `answeredInRound < roundId` — no fresh answer this round |
| `BOUNDED_ANSWER` | `minAnswer`/`maxAnswer` near the price — the **LUNA** failure mode |
| `SEQUENCER_DOWN` / `SEQUENCER_GRACE` | An L2 whose sequencer is down, or back up but not long enough |
| `DESCRIPTION_MISMATCH` | An address filed under the wrong pair |
| `DECIMALS_MISMATCH` / `NON_STANDARD_DECIMALS` | Consumers hardcoding `1e8` |

`BOUNDED_ANSWER` is the one worth explaining. An aggregator with circuit breakers
*cannot* report outside them. When LUNA fell through its floor, the feed kept returning
the floor — fresh, well-formed, and orders of magnitude wrong. Seeing it requires
resolving the proxy and reading the bounds off the implementation, which is what
`alchem-link inspect` does.

## Heartbeats are measured, not copied

This registry used to declare `3600` for every feed, inherited from Ethereum mainnet.
That is wrong almost everywhere, and wrong in the dangerous direction:

| Network | Real ETH/USD heartbeat | A 3600s check would… |
|---|---|---|
| Polygon | **60s** | miss a dead feed for an hour |
| Optimism / Base | **1200s** | miss it for 40 minutes |
| Arbitrum (USDC) | **300s** | miss it for 55 minutes |
| Ethereum | 3600s | be correct |

Every heartbeat in the registry now comes from walking that feed's own round history.
The trick is that Chainlink publishes on *either* a heartbeat or a deviation threshold,
and the two leave different fingerprints — intervals pile up against a ceiling (the
heartbeat), and anything arriving well inside it was triggered by price movement.

```bash
alchem-link cadence ETH/USD -n optimism
```

```
ETH/USD  (optimism)   [matches]
  declared   20m
  observed   20m   (ceiling 1212s, median 442s over 39 intervals)
  triggers   8 by heartbeat, 31 by deviation
  deviation  threshold is at most 0.1509% (largest move seen 0.407%)
```

It also knows when it *cannot* tell you. On a fast L2 the price may never sit still long
enough for the clock to trigger a publish, and the window's longest gap is then a lower
bound, not a measurement:

```
ETH/USD  (arbitrum)   [not observed]
  every round in this 2941s window was deviation-triggered, so the heartbeat was never
  exercised — it is at least 451s, and the declared 3600s is not contradicted.
```

Feeds whose heartbeat is a bound rather than a measurement are marked `*` in
`alchem-link feeds`.

## Cross-chain divergence

ETH/USD is not one number. It is a separate oracle deployment per chain, and they do not
agree:

```bash
alchem-link divergence ETH/USD
```

```
ETH/USD   [agree]   consensus 1,923.88
  ethereum            1,929.60     +29.7 bps         48m 19s old
  linea               1,926.54     +13.8 bps         1h 49m old
  scroll              1,925.97     +10.9 bps         1h 22m old
  bnb                 1,921.96     -10.0 bps         43s old
  polygon             1,923.07      -4.2 bps         8s old
  ...
  10 chains agree — worst leg 29.7 bps from consensus (threshold 50)
```

Stale legs are excluded from consensus — a feed past its heartbeat is not evidence about
the current price — and outliers are attributed, so a lagging chain is not confused with
a broken one. **Testnets are excluded entirely**: Sepolia's feeds carry unrelated data,
and averaging them in produces a consensus that describes nothing.

## Generate a consumer that passes the audit

```bash
alchem-link generate ETH/USD -n base
```

Emits dependency-free Solidity with the address, the *measured* heartbeat, and — because
the target is a rollup — the sequencer gate and grace period:

```solidity
uint256 public constant MAX_AGE = 1380;   // measured 1200s heartbeat + 15% slack

if (sequencerStatus != 0) revert SequencerDown();
if (sequencerStartedAt == 0) revert SequencerDown();
if (block.timestamp - sequencerStartedAt <= GRACE_PERIOD) revert GracePeriodNotOver(...);

if (updatedAt == 0) revert IncompleteRound();
if (block.timestamp - updatedAt > MAX_AGE) revert StalePrice(updatedAt, MAX_AGE);
if (answer <= 0) revert InvalidPrice(answer);
if (answeredInRound < roundId) revert StaleRoundAnswer(answeredInRound, roundId);
```

Generate the same feed on Ethereum and the sequencer lines are absent — they would be
noise on an L1. `--lang typescript` and `--lang python` emit equivalents.

## Would your guards have caught it?

`audit` tells you about a feed as it is right now. `simulate` asks the question that
actually decides whether a protocol survives: **given the checks your contract performs,
what would have happened?**

```bash
alchem-link simulate
```

```
scenario          result  accepted  what it takes to catch
healthy           clean   8/8       a good guard accepts every round here
bounded_crash     MISSED  10/10     needs a consumer-side price bound or a move limit — staleness will not catch it
frozen_feed       caught  5/10      caught by a staleness window sized to the real heartbeat
sequencer_outage  MISSED  10/10     needs the sequencer uptime gate; the price feed answers throughout
carried_rounds    MISSED  10/10     needs answeredInRound >= roundId
flash_spike       MISSED  7/7       needs a move limit; the spike is fresh, positive and complete
incomplete_round  caught  5/6       needs the updatedAt != 0 check
clock_skew        caught  5/6       needs signed age arithmetic; unsigned subtraction underflows here

4/8 scenarios handled (50%)
gaps: bounded_crash, sequencer_outage, carried_rounds, flash_spike
try --strict, or turn on the specific guard each gap names
```

That is the *default* guard — a staleness window and a positivity check, which is what
most integrations have. `--strict` turns everything on and handles all eight. `--naive`
is `latestRoundData()` and nothing else, and handles two.

`bounded_crash` is the one worth staring at. It is the LUNA shape: the price falls
through the aggregator's `minAnswer` and the feed pins to the floor. Every observation
after that is **fresh, positive, complete, and orders of magnitude wrong**. Staleness
cannot see it. Only a consumer-side bound or a move limit can.

There is a healthy control in the set on purpose — without it, a guard that rejects
everything would score perfectly.

Then check the other direction, against real history:

```bash
alchem-link backtest ETH/USD -n base --strict
```

Rejections there are false positives: rounds the feed legitimately produced that your
guard would have thrown away. A guard that scores 8/8 on the scenarios and rejects a
third of real history is not a guard anyone can ship.

## Statistics that account for how oracles publish

```bash
alchem-link stats ETH/USD -n base
```

```
ETH/USD  (base)  30 rounds over 8h 20m
  last          1,930.24
  change          +0.412%
  range         1,918.00 – 1,944.51   (1.38%)
  twap          1,926.88
  vs twap            +17 bps
  volatility        31.4% annualised
  max drawdown     0.884%
  largest move      94.2 bps
  interval      median 20m
```

Two of those numbers are computed differently from how a naive implementation would.

**TWAP is time-weighted, not sample-weighted.** An oracle publishes on a heartbeat *or*
on a deviation threshold, so it prints most often precisely when the price is moving —
which means the mean of the answers systematically over-weights volatile periods. A price
that sat at 1,900 for fifty minutes and then walked to 1,950 over six rounds has a sample
mean near 1,940 and a time-weighted mean near 1,905. The second is what a TWAP oracle
would have reported.

**Volatility is scaled by measured spacing.** Annualising needs the sampling interval;
assuming one is how the same asset reports wildly different volatility on Polygon (60s
publishes) and Ethereum (3600s). The interval comes from the timestamps.

## Commands

**Live** — `price` · `feeds` · `audit` · `inspect` · `history` · `updates` · `stats` ·
`cadence` · `divergence` · `sequencer` · `watch` · `gas` · `holdings` · `transfers` ·
`block` · `doctor` · `verify` · `ccip`

**Offline** — `search` · `networks` · `coverage` · `simulate` · `backtest` · `theme`

**Interactive** — `ui` · `shell` · `chat` · `providers`

**Reference and codegen** — `generate` · `alchemy` · `chainlink` · `integration` ·
`blueprint` · `recipes`

Every command takes `-n/--network`, `--rpc-url`, `--no-color` and `--format`.

### `--format` — one flag, every command

Every result object exposes `as_dict()`, so anything that returns a list can be emitted
as `json`, `ndjson`, `csv`, `markdown` or a **Prometheus** scrape body:

```bash
alchem-link feeds --live -n polygon --format prometheus
```

```
# HELP alchem_link_feed_stale 1 when the answer is older than its heartbeat plus tolerance
# TYPE alchem_link_feed_stale gauge
alchem_link_feed_stale{network="polygon",pair="ETH/USD"} 0
alchem_link_feed_age_seconds{network="polygon",pair="ETH/USD"} 41
```

Scrape that on a timer and `alchem_link_feed_stale == 1` is a complete alert rule — the
tool stops being something you run when you are worried and starts telling you when to
be.

## Batched reads

Reading a network's feeds means three `eth_call`s each. Done naively that is 48 round
trips on Ethereum — about 20 seconds on a public endpoint. The toolkit collapses them
through **Multicall3**, falls back to **JSON-RPC batching** where Multicall3 is not
deployed, and reports which tier it used:

```
ethereum    16/16 OK   607ms  http=2 logical=50
```

The tier matters beyond speed. Multicall3 executes every sub-call in one EVM invocation,
so all 48 reads come from the *same block* — without which comparing two feeds means
comparing two different moments.

## Verified registry — 66 feeds, 11 networks

Every address ships only after being called for `description()` and `decimals()`, and is
filed under the pair the contract itself reports. That check keeps earning its place:

- The address widely shared as Base **"BTC/USD"** reports `WBTC / USD` — a wrapper that
  can depeg. Registered under its real name, with a note.
- The Gnosis address commonly labelled **"xDAI/USD"** reports `DAI / USD`.
- Two candidate CCIP routers had **no code at all** and were dropped.

```bash
alchem-link verify -n base
```

| Network | Chain ID | Feeds | |
|---|---|---|---|
| `ethereum` | 1 | 16 | CCIP |
| `sepolia` | 11155111 | 3 | testnet, CCIP |
| `base` | 8453 | 6 | L2, uptime feed, CCIP |
| `arbitrum` | 42161 | 8 | L2, uptime feed, CCIP |
| `optimism` | 10 | 6 | L2, uptime feed, CCIP |
| `polygon` | 137 | 7 | CCIP |
| `avalanche` | 43114 | 5 | CCIP |
| `bnb` | 56 | 5 | CCIP |
| `gnosis` | 100 | 4 | |
| `scroll` | 534352 | 3 | L2 |
| `linea` | 59144 | 3 | L2 |

Endpoint resolution, most explicit first: `--rpc-url`, then `ALCHEMY_URL`, then
`ALCHEMY_API_KEY` combined with the network's Alchemy subdomain, then a keyless public
endpoint. `doctor` always reports which one is in use, with any key redacted.

## Gas, priced in dollars

`eth_gasPrice` returns one number and hides the structure. `eth_feeHistory` returns the
base fee, the priority-fee percentiles, and — because EIP-1559 fixes it from the parent
block — the next block's base fee as *fact*, not forecast. And since the package already
reads Chainlink, the estimate converts through the native token's feed on the same chain:

```bash
alchem-link gas
```

```
ethereum  (20 blocks sampled)
  base fee   0.2057 gwei  →  next block 0.2215 gwei [rising]
  congestion 48% of target
  ETH/USD    1,929.60

  tier         tip (gwei)   max (gwei)    transfer        swap
  slow             0.0009       0.4438     $0.0090     $0.0772
  standard         0.1500       0.5930     $0.0151     $0.1290
  fast             1.8970       2.3400     $0.0858     $0.7358
```

That composition is the whole Alchemy-plus-Chainlink premise in one command: chain state
from the node, valuation from the oracle.

## CCIP

CCIP does not address chains by chain ID. It uses 64-bit **chain selectors** — Ethereum
is chain ID 1 and selector 5009297550715157269 — and passing one where the other belongs
compiles, deploys, and reverts. Every router here answers `typeAndVersion()` as
`Router 1.2.0`, and every lane is confirmed against the router's own `isChainSupported`:

```bash
alchem-link ccip -n base
```

```
router 0x881e3A65B4d4a04dD529061dd0071cf975F58bCD  (base)
  arbitrum     selector 4949039107694359620    open
  ethereum     selector 5009297550715157269    open
  sepolia      selector 16015286601757825753   closed
```

## Python API

One object holds a network, a connection and a per-feed cache, so a session is one client
rather than one per call:

```python
from alchem_link import connect

link = connect("base")
link.price("ETH/USD")            # one round trip
link.price("ETH/USD")            # cached — no round trip
link.audit()                     # reuses the same client and Multicall3 probe
link.stats("ETH/USD")            # TWAP, volatility, drawdown over recent history
link.everywhere("ETH/USD")       # every chain that carries it, concurrently
link.rpc_stats()                 # what the whole session actually cost
```

Cache TTLs come from each feed's **measured** heartbeat, not a constant — a Polygon feed
on a 60-second cadence caches for 20 seconds, an hourly mainnet feed for two minutes.
`price(..., strict=True)` raises `StaleFeed` instead of returning a reading that says so,
for the contract-facing paths where forgetting to check `.stale` is the bug.

Every exception descends from `AlchemLinkError`, and the ones that replaced a builtin
still inherit it — `UnknownNetwork` is a `KeyError`, `AbiError` is a `ValueError` — so
nothing that caught them before stops working.

The functional API is unchanged:

```python
from alchem_link import read_feed, audit_feed, profile_feed, compare_pair

reading = read_feed("ETH/USD", network="polygon")
if reading.stale:
    raise RuntimeError(f"{reading.pair} is {reading.age_secs}s old — refusing to trade")

audit = audit_feed("ETH/USD", network="base")
if not audit.safe_to_consume:
    for finding in audit.sorted_findings:
        print(finding.severity, finding.code, finding.detail)

profile = profile_feed("ETH/USD", network="arbitrum", rounds=120)
print(profile.observed_heartbeat, profile.heartbeat_verdict)

report = compare_pair("ETH/USD")
print(report.spread_bps, [leg.network for leg in report.outliers])
```

Batched reads of arbitrary contracts, with the ABI codec exposed:

```python
from alchem_link import Call, batch_call, client_for, encode_call, selector

print(selector("latestRoundData()"))          # 0xfeaf968c — computed, not stored
rpc = client_for("ethereum")
report = batch_call(rpc, [
    Call(token, "balanceOf(address)", (wallet,), ["uint256"], "balance"),
    Call(token, "symbol()", (), ["string"], "symbol"),
])
print(report.tier, report.block_atomic, report.by_label("balance").one())
```

History from event logs rather than by walking rounds — one `eth_getLogs` instead of a
hundred `eth_call`s — plus the statistics over it:

```python
from alchem_link import Series, answer_updates, summarise

updates = answer_updates(address, hours=24, network="base")
stats = summarise(Series.from_updates(updates, "ETH/USD", "base"))
print(stats.twap, stats.volatility_annual, stats.max_drawdown_pct)
```

Fan a read across every chain that carries a pair, concurrently, with failures reported
as rows rather than raised:

```python
from alchem_link import read_pair_everywhere

sweep = read_pair_everywhere("ETH/USD")
print(sweep.values(), sweep.failed, f"{sweep.speedup:.1f}x")
```

Replay your consumer's guards, offline:

```python
from alchem_link import Guard, audit_guard

result = audit_guard(Guard(max_age_secs=3600, require_positive=True))
print(result.score, result.failed)     # 0.5 ['bounded_crash', 'sequencer_outage', ...]
```

## Zero dependencies, including the hash

`hashlib` ships SHA3-256, which is **not** Keccak-256 — NIST changed the
domain-separation byte between the Keccak submission and the final standard, and
Ethereum froze on the original. That single byte is why so much Ethereum tooling reaches
for a native extension.

This package implements Keccak-256 in about a hundred lines of integer arithmetic, so
function selectors are **computed rather than trusted**:

```python
>>> selector("getRoundData(uint80)")
'0x9a6fc8f5'
```

That is what makes the ABI codec possible — including the `(address,bool,bytes)[]`
tuple-array encoding that `Multicall3.aggregate3` requires — and it is pinned in tests
against the standard vectors *and* the four selectors this package previously shipped as
hand-verified constants.

As of 0.23.0 there is no optional extra either. The dashboard used to need `textual`,
which made the user interface the one place the zero-dependency claim was quietly
abandoned. It is not any more — see below.

One practical note baked into the client: several public RPC providers reject Python's
default `Python-urllib/3.x` User-Agent with a 403, which reads as "the chain is down" if
you have not hit it before. The client always sends a real one.

## The terminal system

```bash
pip install alchem-link
alchem-link ui
```

```
  Alchem-Link v0.23.0                              base  ·  66 feeds  ·  truecolor
 ALCHEM-LINK      ┏━ Live Feeds ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
                  ┃ETH/USD            1,930.24  FRESH      3m 21s ago · hb 20m  ┃
 Live Feeds       ┃WBTC/USD          67,940.11  FRESH      1m 04s ago · hb 20m  ┃
 Safety Audit     ┃  Wrapped BTC, not spot BTC — can depeg.                     ┃
 Analytics        ┃USDC/USD               1.00  FRESH     41m 12s ago · hb 1d   ┃
 Cross-chain      ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
 ...
 Live Feeds · base          ↑↓/jk scroll · tab pane · n network · r refresh · q quit
```

Fifteen panels, including the guard simulator and registry coverage. Every panel that
talks to a chain does so on a worker thread, so an RPC round trip never freezes the app,
and results cache per panel until `r`. `n`/`N` cycle networks, `tab` moves focus between
the rail and the panel, `1`-`9` jump, `q` quits.

### It is black and blue before the first frame

Drawing black rectangles gets you a black *pane*. The columns past the last painted cell,
the scrollback above the prompt, and anything a subprocess writes all stay whatever colour
the terminal was. So `alchem_link.term.boot` repaints the terminal's **own defaults** via
OSC 11/10/12 — background, foreground and cursor — and hands them back on exit. That runs
from `alchem-link`, from `alchem-link shell`, and from the compiled binary, which is why
plain `alchem-link price ETH/USD` output sits on the same surface the dashboard does.

Colour is negotiated per stream and degrades rather than breaking: truecolor → xterm-256
→ the basic sixteen → none. `NO_COLOR`, a pipe, or `--no-color` each reduce output to
plain text **with the layout intact** — the tests assert the two are character-identical,
because this output goes into CI logs and issue reports at least as often as onto a
screen. On Windows, VT processing is enabled through the Win32 API first; without that
call the whole UI renders as visible `←[38;2;…m` garbage.

### What is actually in there

| Module | Job |
|---|---|
| `term/ansi.py` | escape sequences, colour-depth negotiation, display-width measurement |
| `term/screen.py` | double-buffered cell grid emitting only the runs that changed |
| `term/input.py` | raw mode, and a pure parser from escape sequences to named keys |
| `term/widgets.py` | panels, tables, sparklines, gauges, tabs, scroll state |
| `term/app.py` | event loop, worker pool, resize handling |
| `term/boot.py` | terminal initialisation — and putting it back |

An idle frame costs **zero bytes**: the screen diffs the back buffer against what is
actually on the terminal and emits one cursor move per changed run. That is the difference
between a dashboard that is usable over SSH and one that is not.

The layering is strict — `ansi` knows about bytes, `screen` about cells, `widgets` about
rectangles, `app` about events — and nothing in the subpackage imports anything from
`alchem_link` except the inert palette, so it can be read and reused on its own.

### Testable without a terminal

Panels render to a list of `(text, style)` lines; the app paints a window onto that list.
Scrolling and clipping are one slice on one list, and every renderer is a pure function —
so `tests/test_dashboard.py` exercises all fifteen panels in their loading, empty and
error states without a terminal in sight. That matters more here than usual: a dashboard
that crashes takes the whole screen with it.

### Standalone binaries

No Python needed on the target machine, and nothing to collect — the package has no
dependencies to bundle:

```bash
pip install pyinstaller
pyinstaller alchem-link.spec        # the CLI
pyinstaller alchem-link-ui.spec     # the dashboard
```

A binary launched by double-click lands in a fresh console with no `TERM` at all, which is
where colour detection has the fewest hints and where theming matters most. `boot` detects
the frozen case and themes it anyway.

### The palette

`alchem_link/theme.py` is the single source: an inert table of hex values and semantic
`Style` roles, with no escape sequences in it. `ansi` encodes a role for whatever depth is
available; `render` uses the same roles for line output; the web build under
`web/lib/alchem/` mirrors the same values.

`tests/test_theme.py` fails the build if any render module hardcodes a colour, if a role
paints on an undefined surface, or if the three status colours collapse into each other
once quantised to 256 or 16 colours — which is how a "harmless" palette tweak stops a
STALE badge from being distinguishable on a bare console.

```bash
alchem-link theme     # the palette, and what this terminal negotiated
```

## Web build

The same reader runs at `/alchem-link` in the Scematica dashboard (`web/`), porting
`abi.py`, `networks.py` and `feeds.py` to TypeScript under `web/lib/alchem/` and serving
routes that read aggregators **server-side**, so `ALCHEMY_API_KEY` never reaches the
browser and CORS-less public RPC hosts still work.

This package stays authoritative. When the registry here changes, change
`web/lib/alchem/feeds.ts` too — `/api/alchem/verify` is what catches the two drifting,
because it asks the chain rather than either table.

## Exit codes

| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Completed, but the result is not usable — a stale feed, a failed check, a divergence |
| 2 | Usage error — unknown pair, unknown network, missing argument, missing API key |
| 3 | Network error — endpoint unreachable |
| 4 | RPC error — the node answered with an error |

`price` exits non-zero on a stale read and `audit` on any finding at medium or worse, so
both drop straight into a CI gate.

## Tests

```bash
python -m unittest discover -s tests
```

479 cases, all offline — no test in this suite reads a chain.

That is a design constraint rather than a convenience. `analytics` and `simulate` compute
numbers people size positions and write guards against, and a number that can only be
checked against a live chain cannot be checked at all. The RPC transport is stubbed at the
single-attempt boundary so the real retry policy stays under test; every dashboard panel is
rendered in its loading, error and empty states; and the terminal engine is exercised
against an in-memory screen, including the cases that are invisible until they are
catastrophic — a diff that reports "nothing changed" when something did, a wide character
that shifts every cell after it by one column, and an escape sequence emitted at a terminal
that would render it as literal digits.

## Requirements

Python 3.10 or later. Nothing else.

The dashboard wants a terminal, but does not require much of one: it negotiates down to
256 colours, to the basic sixteen, and to no colour at all, and every command works
unstyled through a pipe.

## License

MIT
