Farol
by ronoel
README.md
# Farol
**The registry warns, the wallet signs.**
Farol is Portuguese for lighthouse: it warns of the rocks, it does not promise the ship a clear
crossing.
Farol is an MCP server that gives an AI agent a curator-signed catalog of smart contracts and
turns an intent into a transaction plan a human reviews on a local page before signing. **The MCP
process never holds a private key.** No MCP tool signs a transaction or broadcasts one; signing
happens only on the review page, driven by the wallet already injected in the user's browser.
Built for the EAG Global Buildathon Floripa, on HashKey Chain.
---
## Submission
EAG Global Buildathon Floripa, 12 September 2026 · https://devfolio.co/projects/farol-1d90
| Track | Why |
|---|---|
| **Brazil Hackathon** | Built in person at InPETU Hub, Sapiens Parque, Florianópolis. |
| **EAG track 4 — Application Middleware & Open-Source Tooling** | Farol is not an application, it is the layer under one: an MCP server any agent host can mount, a documented signed-catalog format, and a review page any wallet or agent framework can adopt without adopting Farol. |
| **HSK Chain — Blockchain Infrastructure** (also fits AI Agents) | See *Technical integration: HashKey Chain* below. No contract of ours is deployed; the product runs against contracts already on HSK. |
Where the rest lives: architecture in *Architecture* and *How it works*; features in *Key features* and
*The tools*; install and run in *Install* and *Run the demo*; what is proven and what is not in *What is verified*
and *Limits*; iteration plans in *Roadmap*.
## The problem
An agent that can transact has to answer a question no chain answers for it: is this address the
contract it claims to be? Today the agent guesses from a name, and the human is asked to approve
calldata they cannot read.
Name collisions are not hypothetical. On HashKey Chain two verified contracts share the name
"HyperIndex V3 SwapRouter". A contract whose verified source calls itself "Mock USDC" reports its
`symbol()` as exactly `USDC`.
## How it works
1. The agent calls `plan_build` with a catalogued runbook, explicit calls, or a revoke request.
2. The server resolves the plan into `{to, data, value}` calls, computes a **tier** per call by
reading `extcodehash` live, and stores the result as an immutable plan on disk, keyed by the
hash of its canonical JSON.
3. A review URL on `127.0.0.1` opens. The page fetches the plan **from the server by id**, never
from the agent or the URL.
4. The page runs `eth_simulateV1` when it opens and re-checks flags at sign time, so a flag raised
after the plan was built still blocks the sign button for the affected call.
5. The human reads the tier, the simulation and the trust line, then signs with their own wallet.
The page posts the `txHash` back to the server; the agent learns it by polling `plan_status`.
## Key features
**Four tiers, one refusal.** Every call carries a tier computed against the chain, not against a
cached list:
| Tier | Meaning |
|---|---|
| T0 | catalogued, codehash matches what a curator signed |
| T1 | verified source, no curated entry |
| T2 | unverified source |
| T3 | catalogued address, deployed code does **not** match — hard refusal |
T3 is the only refusal in the system. Everything else proceeds with a badge, because
`eth_simulateV1` runs and a decode is attempted on any bytecode, catalogued or not.
**Proxies.** An entry pins two codehashes: the proxy's and its implementation's. At plan time the
client reads the EIP-1967 implementation slot and compares; if the implementation changed since the
entry was reviewed, the call demotes from T0 to T1 rather than hard-failing — an upgrade is not by
itself an attack. The admin slot is read too, so the page shows who can replace the contract
underneath the user. For UUPS patterns where the slot is empty it says "not determinable from
storage" rather than guessing. Circle's FiatTokenProxy keeps those values in the pre-EIP-1967
ZeppelinOS slots; Farol reads those as well.
**Flags name selectors, not contracts,** so an exit path such as a revoke stays callable on a
flagged contract. A flag blocks signing only when its signer is on the curator trust list and its
severity is not `info`; a flag from anyone else is still shown, never hidden. If the flag feed
cannot be read at all, `plan_build` refuses outright rather than proceeding as if there were no
flags — negative information fails closed.
**Discovery filters, it never ranks.** `catalog_search` returns results alphabetically and always
states how many catalogued entries matched out of how many exist on that chain, plus a note that
uncatalogued contracts may exist. Token symbols resolve only against the curator-signed token list;
an ambiguous symbol is refused rather than guessed.
**The catalog carries the trust model, never a price.** Who can upgrade the code, how funds exit,
who can pause it — and the pause switch is read *live*, because a state cannot be signed in
advance. No yield figure, no recommendation: a registry that carries those becomes a thing to spoof.
**Audits are bound to deployed bytecode.** An audit report scopes to a git commit; what a human
signs is bytecode at an address. Farol keys on `(chainId, address, codehash)` and says on every
read whether an attestation still describes the code that is deployed *now*.
The shipped catalog carries **zero** audit attestations, because nobody has audited these
contracts, and every entry says so.
## The tools
Five MCP tools. Nothing here signs, broadcasts, or marks a plan as signed.
| Tool | In | Out |
|---|---|---|
| `catalog_search` | chain, optional text/address/intent/asset filter | matching entries, alphabetical, ambiguity stated, coverage count |
| `catalog_get` | chain, address | entry, attestations, audit attestations with the codehash each auditor signed over and whether it still matches, active flags, upgrade authority, current tier, and the exit path with the pause switch read live |
| `plan_build` | chain, `from`, one of `{entry, runbook, bindings}` \| `{calls}` \| `{revoke}` | `plan_id`, review URL, tier per call |
| `plan_simulate` | `plan_id` | `eth_simulateV1` over the steps not yet sent: logs and custom errors named with catalogued ABIs, return data, native transfers |
| `plan_status` | `plan_id` | `created` · `reviewed` · `signed` · `confirmed` · `rejected`, plus `txHashes` |
There is no `send`, `sign`, or `mark-signed` tool, by design. Only a local HTTP endpoint the review
page calls (`POST /t/<token>/plan/<id>/result`) can record a `txHash`, and that endpoint is not
reachable from the MCP surface an agent uses.
---
## Install
Prereqs: **Node 20+**.
```
git clone https://github.com/ronoel/farol-mcp.git
cd farol-mcp
npm install
```
### Claude Code
The repo ships `.mcp.json`, picked up automatically from the project root. Add your own signing
address, which is the **operator's** setting and never comes from the agent:
```json
{
"mcpServers": {
"farol": {
"command": "npx",
"args": ["tsx", "src/index.ts"],
"env": { "FAROL_POLICY": "graduated", "FAROL_FROM": "0xYourAddress" }
}
}
}
```
For Claude Desktop, use an absolute path to `src/index.ts`, since Desktop does not run from the
repo's working directory.
### Environment variables
| Variable | Purpose |
|---|---|
| `FAROL_FROM` | The signing address. Set by the operator; if it is absent, `plan_build` refuses and says so. |
| `FAROL_POLICY` | `graduated` (default) or `strict`. Strict allows only T0 calls. |
| `FAROL_CURATOR_KEY` | Curator private key. Read **only** by the signing scripts, never by the running server — the server process never holds a key. |
| `FAROL_PORT` | Fixed port for the review server. Default `0`, a random free port. |
| `FAROL_NO_OPEN` | `1` stops the server from calling `xdg-open` on the review URL. |
| `FAROL_RPC_177` / `FAROL_RPC_133` / `FAROL_RPC_1` / `FAROL_RPC_8453` / `FAROL_RPC_84532` | RPC overrides for HashKey Chain, HashKey Chain Testnet, Ethereum, Base and Base Sepolia. |
| `FAROL_CURATORS` | Override for the curator trust list. Default: `catalog/curators.json`. |
## Run the demo
No MCP host needed. This starts the review server, builds a wrap plan of 0.01 HSK on **HashKey
Chain Testnet (133)** and prints a review URL — open it in the browser that holds your wallet:
```
npm run demo:plan -- <your-address> 133
```
Use `177` for HashKey Chain mainnet. Testnet funds: https://hsk.xyz/faucet
**Rehearse the one refusal.** T3 is a catalogued address whose live code no longer matches the
signed codehash, and a live chain will not produce one on demand. This copies the catalog to a temp
directory, rewrites one codehash to a value the chain does not have, re-signs it with the real
curator key, and prints a `FAROL_CATALOG` to run against. `plan_build` then refuses for real,
naming both hashes. The repo's catalog is never touched.
```
npm run demo:t3 -- 8453 0x4200000000000000000000000000000000000006
```
**Rehearse an audit bound to a deployed codehash:**
```
npm run demo:audit -- 8453 # the attestation matches the deployed code
npm run demo:audit -- 8453 0x4200000000000000000000000000000000000006 --stale
```
---
## Technical integration: HashKey Chain
Farol deploys no contract of its own. It is middleware over contracts that are **already on HSK
Chain**, and everything it says about them is re-read from the chain at plan time.
- **5 curator-signed entries on HSK mainnet (177)**, each pinning a codehash read from mainnet:
- Wrapped HSK `0xB210D2120d57b758EE163cFfb43e73728c471Cf1` — wrap / unwrap.
- Tether USD, OP Stack bridged `0xF1B50eD67A9e2CC94Ad3c477779E2d4cBfFf9029` — the canonical
bridged USDT; withdrawals to L1 burn it, so no approve is needed on that path.
- OpenUSDT, CCIP-bridged `0x1217BfE6c773EEC6cc4A38b5Dc45B92292B6E189` — what a CCIP bridge UI
delivers when it says "USDT". **Not** the Tether USDT the HSK pools use.
- HyperIndex V3 SwapRouter `0xC161Db322043e94D0395673ab5A74F89088EC3b8` — pins the codehash of
one of the two verified contracts sharing this name on HSK. The other has no entry and
resolves at T1.
- Chainlink CCIP Router `0xf2Fd62c083F3BF324e99ce157D1a42d7EbA77f1d` — read-only entry, trust
model recorded as facts, no execution runbook.
- **1 entry on HSK testnet (133)**: the OP Stack WETH predeploy
`0x4200000000000000000000000000000000000006`, whose on-chain `name()` reads "Wrapped HashKey
EcoPoints" — the wrap/unwrap rehearsal.
- **Curator-signed token lists for 177 and 133**, so an agent resolving "USDT" on HSK resolves it
against signed data instead of guessing.
- **Tiering is live against HSK**, not precomputed: every `plan_build` re-reads `extcodehash` on
177 or 133 and compares it to the signed entry.
- **Transactions are built and human-signed on HSK**: the wrap runbook on 133 produces a real
signed transaction through the review page; the swap runbook on 177 produces the two-step
approve + `exactInputSingle` plan, with the approve scoped to the **exact** amount, never
unlimited.
- **4 of the 12 live network tests run against HSK** (chains 177 and 133).
Entries also ship for Ethereum (1), Base (8453) and Base Sepolia (84532), which is what shows the
runbook templates are chain-agnostic rather than hardcoded to one deployment.
## Catalog format
An entry is produced, then signed, then loaded only if the signature checks out:
```
npm run keygen >> .env
# add the printed address to catalog/curators.json (or set FAROL_CURATORS)
npm run entry:make -- <chainId> <address> --kind token --name X
# edit runbooks, intents, attestations and notes by hand in the written JSON
npm run entry:sign -- catalog/entries/<chain>/<file>.json
```
The signature is an EIP-191 `personal_sign` over the **canonical JSON** of the raw body: keys
sorted recursively, `undefined` dropped, no schema defaults applied — so what gets signed is
exactly the bytes on disk. `entry:sign --role author` produces a protocol team's own signature over
the same entry.
## Architecture
```
src/mcp.ts the five tools; the whole surface an agent can reach
src/plan.ts intent or runbook -> {to, data, value} calls, tier per call
src/tier.ts T0/T1/T2/T3 against a live extcodehash, proxy slots included
src/catalog.ts load + verify signatures against the curator trust list
src/canonical.ts canonical JSON: what a signature actually covers
src/chain.ts RPC: extcodehash, storage slots, eth_simulateV1
src/http.ts the local review server; the only path that can record a txHash
src/page/ the review page: plain HTML and JS, no framework
src/store.ts immutable plans on disk, keyed by their own hash
catalog/ signed entries, token lists, flags, curator trust list
```
## What is verified
`npm test` — **8 suites, 108 tests**: canonical JSON and signatures, the tier system, plan-building
rules including flags and revoke, the review server's HTTP rules, the MCP tool surface, audit
attestations and their codehash binding, the review page driven in jsdom against a fake wallet
provider, and repo hygiene (no `console.log` in `src/`, since stdout is the MCP transport; every
shipped catalog document verifying against the shipped curator list).
`npm run test:live` — **12 network tests** against real chains: 4 on HashKey Chain mainnet, HashKey
Chain testnet and Ethereum; 8 on Base Sepolia.
**Not verified, and worth saying so:**
- Signing with a real MetaMask on 133 or 177 is not covered by an automated test; the page's wallet
interaction is tested only against a fake injected provider in jsdom.
- EIP-7702 batching (`wallet_getCapabilities`) is not implemented.
## Limits
`eth_simulateV1` returns logs, return data and native transfers; it does **not** return a storage
diff. A call can move state the simulation does not surface as a log.
A compromised curator key can sign a forged entry, and the codehash check will not catch it — the
attacker computes the codehash correctly. The floor under that failure is the review-screen
invariant: what the human reads on the page derives from calldata, the ABI and the simulation,
never from runbook prose an agent or a curator wrote.
The catalog raises the ceiling; the simulation holds the floor. Neither claims more than what it
checked.
## Roadmap
- A public, read-only Streamable HTTP surface (`catalog_search`, `catalog_get`, `tx_explain`) with
no wallet path, so the catalog is usable without running a server.
- The signed flag feed as a published format with a small reference client, and its timestamp
anchored on Ethereum L1 via EAS.
- Real audit attestations from auditors willing to sign over a deployment rather than a commit —
the format is implemented and shipping empty on purpose.
- EIP-7702 batching, so a multi-step runbook is one signature instead of several.
## License
GNU Affero General Public License v3.0 only (AGPL-3.0-only). See `LICENSE`.
Open source, and deliberately the strongest copyleft: anyone who modifies Farol and runs it as a
network service has to publish their changes under the same licence. The review page is a trust
boundary — a closed fork of it would be a fork nobody can audit, which is the one thing this
project cannot allow. Commercial licences under other terms are available on request.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues