Pharos Sentinel
Evaluates on-chain risk for Pharos agents before executing transactions, providing verdicts (safe/caution/dangerous) and risk-bounded execution plans via Foundry cast reads.
README
Sentinel โ a pre-action on-chain risk gate for Pharos agents
๐ Live demo: pharos-sentinel-production.up.railway.app โ run the risk gate against the live Atlantic fixtures (each /check executes real Foundry cast reads on Pharos Atlantic testnet).
This repository ships the Pharos Skill Engine with Sentinel added as its
Step 0 โ risk pre-check. The engine
(PharosNetwork/pharos-skill-engine)
gives an AI agent the full Pharos on-chain toolkit โ balance/transaction queries, transfers,
contract deploy & verify, and batch airdrops โ driven through Foundry (cast / forge).
Sentinel is the reusable Skill this repo adds on top: an agent calls it before it moves
value and gets a risk verdict (safe / caution / dangerous), the reasons behind
it, and a risk-bounded execution plan. It is read-only โ it advises and blocks; it never
signs or sends a transaction.
A pre-action risk check is the most-called primitive in any on-chain agent stack: every transfer, swap, or approval is a place an agent can lose funds. Sentinel makes that check a single, composable call, and wires it in as the engine's first write-operation pre-check.
What's in this package
This is the Pharos Skill Engine layout, with Sentinel slotted in as a skill:
SKILL.mdโ the engine's agent entry point. Sentinel is registered in the Capability Index and as Step 0 of the Write-Operation Pre-checks.references/โ the engine's command references (query.md,transaction.md,contract.md,script-gen.md) plussentinel.md, the risk-gate reference.assets/โ the engine'snetworks.json(Atlantic testnet + mainnet),tokens.json, ERC-20 / airdrop Solidity templates, and script-generation templates.- Sentinel runtime โ
sentinel_skill.py(MCP server),sentinel_cli.py(CLI),pharos_atlantic.py(RPC reads), plus the demos, live risk gallery, x402 gate, and tests.
Sentinel runs on Atlantic testnet โ matching the engine's default network and its Piggy Bank
reference skill โ while mainnet stays available in networks.json for the engine's other
capabilities.
What makes it different
- Read-only by design. Sentinel never holds keys and never sends a transaction โ the safest possible posture for a Skill that agents trust before moving money.
- Foundry-native execution. Every on-chain read runs through the Foundry
castCLI โ the same toolchain the rest of the Skill Engine uses โ so Sentinel composes into the engine rather than bolting a separate client onto it. No indexer, no third-party API, no keys, no database. - Not just a verdict โ a plan.
execution_planreturns approve/block plus bounded sizing within the caller's risk tolerance, so the agent gets a decision, not just a score. - Real EVM depth. Bytecode opcode analysis and proxy/ownership introspection, not surface heuristics (details below).
How it works
Sentinel sits between an agent and the chain. The agent declares an intent; Sentinel reads
Pharos Atlantic by executing read-only Foundry cast commands (no keys, no transaction), scores
what it finds, and returns a verdict, the reasons, and a risk-gated plan. The agent acts on the plan.
flowchart LR
A["Pharos agent<br/>transfer ยท swap ยท approve ยท call"] -->|"address + action + amount"| S{{"Sentinel Skill<br/>risk_check / execution_plan"}}
S -->|"cast reads ยท no keys ยท no tx"| C[("Pharos Atlantic<br/>chainId 688689")]
C -->|"cast code ยท call ยท storage"| S
S -->|"verdict + reasons + plan"| D{verdict}
D -->|safe| P["Proceed"]
D -->|caution| R["Reduce size / confirm"]
D -->|dangerous| B["Block"]
A blocked approve under a strict safe-only tolerance, end to end (mirrors demo_agent.py):
sequenceDiagram
autonumber
participant Ag as Pharos agent
participant Se as Sentinel
participant Ch as Pharos Atlantic
Ag->>Se: risk_check(addr, "approve", 100)
Se->>Ch: cast code ยท cast call ยท cast storage
Ch-->>Se: bytecode ยท owner ยท impl slot ยท paused
Se-->>Ag: verdict "caution" + reasons
Ag->>Se: execution_plan(addr, "approve", 100, max_risk "safe")
Se-->>Ag: approved=false ยท BLOCK
Note over Ag: agent halts โ no allowance is signed
Two tools
| Tool | Purpose |
|---|---|
risk_check(address, action, amount_phrs?) |
Returns {verdict, score, reasons[], data{}}. |
execution_plan(address, action, amount_phrs, max_risk) |
Risk-gated: approve/block + bounded sizing within tolerance. |
action is one of transfer | swap | approve | call.
Risk signals (v2 โ read via Foundry cast)
- Contract vs EOA, and action/target mismatches (e.g.
approveto a non-token, or to a wallet). - Proxy detection: EIP-1167 minimal proxies and EIP-1967/1822 upgradeable proxies (the owner can swap the logic after you interact โ a real rug vector).
- Bytecode opcode analysis: a proper opcode walk (stepping over PUSH immediates) that flags SELFDESTRUCT and DELEGATECALL used outside a known proxy pattern.
- Ownership & upgrade-admin concentration:
owner()+ the EIP-1967 admin slot, distinguishing an EOA owner (higher centralization risk) from a contract owner (likely multisig/timelock). - ERC-20 introspection:
symbol/decimals/totalSupply, with a zero-supply-trap flag. - Pausable state (
paused()), tiny-bytecode stubs, and brand-new / zero-history counterparties (a typo & address-poisoning guard).
Every check starts at a perfect 100 and each signal subtracts from it, so risks stack into a
single safety score (100 = safest, 0 = riskiest). The verdict is a band over it
(>=66 safe, 31โ65 caution, <=30 dangerous). The band turns the score into the verdict, and
execution_plan turns that into a decision:
flowchart TD
addr["address + action"] --> isC{"has bytecode?"}
isC -->|"no โ EOA"| eoa["EOA signals<br/>fresh / no history ยท action/target mismatch"]
isC -->|"yes โ contract"| con["Contract signals"]
con --> px["proxy<br/>EIP-1167 ยท 1967 ยท 1822"]
con --> bc["bytecode<br/>SELFDESTRUCT ยท DELEGATECALL"]
con --> own["ownership / upgrade-admin<br/>EOA vs contract"]
con --> tok["ERC-20<br/>supply ยท zero-supply trap"]
con --> st["pausable ยท tiny stub"]
eoa --> sc(["safety score<br/>100 โ risks"])
px --> sc
bc --> sc
own --> sc
tok --> sc
st --> sc
sc --> band{"safety band"}
band -->|"<= 30"| dg["dangerous"]
band -->|"31 - 65"| ca["caution"]
band -->|">= 66"| sf["safe"]
dg --> plan["execution_plan<br/>approve / block + bounded size"]
ca --> plan
sf --> plan
Use it two ways
As an MCP server โ for MCP-capable agents:
pip install -r requirements.txt
python sentinel_skill.py # stdio MCP server exposing risk_check + execution_plan
As a framework Skill (SKILL.md) / CLI โ for Claude Code / OpenClaw / Codex style agents:
python sentinel_cli.py <address> <action> # verdict (JSON)
python sentinel_cli.py <address> approve --plan --max-risk safe # risk-gated plan
The CLI exits 0 for safe/caution (or an approved plan) and 2 for dangerous/blocked, so a
shell or agent can branch on the exit status alone. See SKILL.md for the skill definition.
Quickstart
Live reads require Foundry (cast) on your PATH (curl -L https://foundry.paradigm.xyz | bash && foundryup).
The offline tests and the --synthetic tour need neither Foundry nor network.
python -m unittest test_sentinel # 34 deterministic offline tests (no network, no Foundry)
python feature_tour.py --synthetic # walk every signal instantly (no network)
python demo_agent.py # an agent drives the Skill over MCP against live Atlantic
python -c "import pharos_atlantic as p; print('chain_ok:', p.chain_ok())"
Sample output
$ python sentinel_cli.py 0x24f3cd306c85903ca2ccd0ee8dc1c74111151b23 call
{
"verdict": "caution",
"score": 65,
"reasons": ["tiny bytecode (1 bytes) โ likely a stub/trap rather than a working contract"],
"data": { "is_contract": true, "code_size": 1 }
}
Live demo
Drive five Sentinel features as five real Atlantic transactions, one command each:
python demo.py deploy # deploy a malicious contract -> Sentinel flags it DANGEROUS
python demo.py upgrade # swap a proxy's logic in one tx -> verdict escalates
python demo.py pause # pause a contract -> verdict escalates
python demo.py transfer # execution_plan sends real PHRS only when safe
python demo.py x402 # pay-per-query risk check over x402
python demo.py all # all five, with a transaction summary
Every run deploys fresh contracts and sends fresh transactions โ it's live, not a recording. Full
runbook (commands, suggested patter, expected output) in DEMO.md.
Live on Pharos Atlantic
Sentinel integrates with Pharos Atlantic Testnet via live Foundry cast reads:
- RPC
https://atlantic.dplabs-internal.comยท chainId 688689 ยท explorerhttps://atlantic.pharosscan.xyzยท gas token PHRS
Live risk gallery
To prove the engine against real bytecode (not mocks), a spectrum of decoy contracts was
deployed on Atlantic, each engineered to trip a different signal. Sentinel reads them live and
returns a monotonic safe โ caution โ dangerous ladder โ reproduce it yourself with python gallery.py:
| Exhibit | Action | Verdict | Score | Signal demonstrated |
|---|---|---|---|---|
| CleanToken | transfer | ๐ข safe | 0 | clean ERC-20, no privileged owner โ baseline, no false alarm |
| MinimalProxy | call | ๐ข safe | 10 | EIP-1167 minimal proxy detected |
| TinyStub | call | ๐ก caution | 35 | tiny-bytecode stub / trap |
| ZeroSupplyToken | transfer | ๐ก caution | 40 | zero-supply token trap + EOA owner |
| UpgradeableProxy | call | ๐ก caution | 50 | EIP-1967 upgradeable + EOA owner + paused |
| Backdoor | call | ๐ด dangerous | 70 | SELFDESTRUCT + unguarded DELEGATECALL + paused |
Each verdict above is produced live, on-chain. The address/verdict map lives in
fixtures.json; gallery.py re-checks every exhibit and fails on any drift. The
Solidity-backed exhibits (CleanToken, ZeroSupplyToken, UpgradeableProxy, Backdoor) have verified
source on Pharos Scan โ open any address and check the Contract tab; sources are in
fixtures/.
Live upgrade attack โ Sentinel catches a rug as it happens
The gallery above is static. To prove Sentinel reads live, mutable state โ and to demonstrate the exact threat it warns about โ a mutable EIP-1967 proxy was deployed pointing at benign logic, then upgraded on-chain to hostile logic in a single transaction. Sentinel read the same proxy address before and after:
| Implementation | Verdict | What Sentinel sees | |
|---|---|---|---|
| Before | benign logic | ๐ข safe (80) | upgradeable โ owner can swap the logic after you interact |
| After (upgrade tx) | hostile logic | ๐ก caution (50) | + an EOA owner now holds privileged control + the contract is now PAUSED |
The proxy address never changed โ
0x22Aaโฆd27A โ
but its implementation, ownership, and pause state did. That is the upgrade-rug vector demonstrated
end to end: because Sentinel reads on-chain state at call time, its verdict reflects the swap the
moment it lands. The warning it prints before an attack is the risk that becomes the attack.
Live pause flip โ Sentinel tracks operational state
A second live mutation, on a plain (non-proxy) contract. It carries a latent SELFDESTRUCT
(โ25 โ safety 75, still inside the safe band on its own); the operator then pauses it in a single transaction,
and Sentinel, reading state at call time, tips to caution:
paused() |
Verdict | What Sentinel sees | |
|---|---|---|---|
| Before | โ | ๐ข safe (75) | latent SELFDESTRUCT |
| After (pause tx) | true | ๐ก caution (55) | + the contract is now PAUSED |
Same contract โ 0xE84fโฆ410B โ
different live state, different verdict.
The gate moves real value
execution_plan is not advisory theatre โ it decides whether value actually moves.
guarded_transfer.py asks Sentinel, then sends a real PHRS transfer only
when the plan is approved:
- โ
vetted counterparty โ
safeโ approved โ 0.0005 PHRS sent - โ the live
Backdoorfixture โdangerousโ blocked โ no transaction is signed
The Skill stays read-only; only the agent signs. One approval moved value on-chain; one block stopped it before a transaction existed.
Paid calls via x402
Pharos lists "pay-per-query supplier / supply-chain risk assessment" as a flagship x402
use case โ which is exactly what Sentinel is. So risk_check is also exposed behind an
x402 paywall: an unpaid request gets 402 Payment Required; the agent pays a micro-transfer
on Atlantic; the retry returns the verdict plus the settlement tx_hash.
python sentinel_x402.py # read-only x402 gate on 127.0.0.1:4021
python x402_demo.py # 402 -> pay on Atlantic -> 200 verdict -> replay rejected
The gate verifies payment with the same Foundry cast reads Sentinel uses for risk, so the
server stays read-only and keyless (only the client sends value) and adds no new dependencies
beyond Foundry + the Python stdlib. Full design and the official @x402 SDK path: X402.md.
Verify it yourself
Nothing here is a recording. Sentinel only reads public Pharos Atlantic state, so you can independently confirm both the verdicts and that it never writes.
1. Offline tests โ no network, no Foundry:
python -m unittest test_sentinel # 34 deterministic tests pin the verdict logic
2. Live risk gallery โ read-only (needs Foundry cast):
python gallery.py # re-checks the 6 deployed fixtures; fails on any drift
Expect a monotonic safe โ caution โ dangerous ladder, all six matching fixtures.json.
3. Check any address yourself:
python sentinel_cli.py 0x75fb8b091A7A88bAF14F23Eac2F33962A4Cdd35D call # the Backdoor fixture โ dangerous (exit 2)
4. Reproduce Sentinel's reads by hand โ proof it just reads public chain data via Foundry, nothing hidden:
RPC=https://atlantic.dplabs-internal.com
# the bytecode Sentinel scans (contains SELFDESTRUCT / unguarded DELEGATECALL):
cast code 0x75fb8b091A7A88bAF14F23Eac2F33962A4Cdd35D --rpc-url $RPC
# the pause flag it reads:
cast call 0x75fb8b091A7A88bAF14F23Eac2F33962A4Cdd35D "paused()(bool)" --rpc-url $RPC
# the EIP-1967 implementation slot of the UpgradeableProxy fixture:
cast storage 0xE7797e15DEb86931d7F7b940684Ed1edc5cC7513 \
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc --rpc-url $RPC
These are exactly the reads documented in references/sentinel.md โ Sentinel just
folds them into a single verdict. (All are cast call / code / storage; never cast send.)
Security posture
The Skill is intentionally minimal and auditable. It executes only read-only Foundry cast
commands (cast code / call / storage / balance / nonce / chain-id, plus cast rpc
for tx/receipt lookups) against a single Pharos RPC endpoint โ the same execution model the rest
of the Skill Engine uses. It holds no private key, never signs or sends a transaction, makes no
filesystem writes, and reads no secrets or environment beyond the RPC URL.
Files
| File | Role |
|---|---|
sentinel_skill.py |
MCP server โ tools risk_check, execution_plan |
pharos_atlantic.py |
Pharos Atlantic config + read-only Foundry cast read/introspection helpers |
sentinel_cli.py |
Thin CLI wrapper for SKILL.md / framework agents |
SKILL.md |
Skill definition for Claude Code / OpenClaw / Codex |
demo.py |
Live demo driver โ one subcommand per on-chain feature (deploy/upgrade/pause/transfer/x402) |
DEMO.md |
Live demo runbook โ commands, suggested patter, expected output |
demo_agent.py |
Demo agent driving the Skill over a real MCP connection against live Atlantic |
guarded_transfer.py |
Agent that sends a real PHRS transfer only when execution_plan approves |
feature_tour.py |
Guided walkthrough of every risk signal |
gallery.py |
Re-checks the live Atlantic risk-gallery fixtures and flags drift |
fixtures.json |
Deployed gallery addresses + expected verdicts |
sentinel_x402.py |
x402 paid-call gate โ risk_check behind HTTP 402 (read-only verify) |
x402_demo.py |
Drives the full x402 pay-per-query loop on live Atlantic |
X402.md |
x402 design: native verify-by-RPC + the official @x402 SDK path |
references/sentinel.md |
Sentinel's engine reference file โ risk gate + Foundry (cast) read equivalents |
references/{query,transaction,contract,script-gen}.md |
Pharos Skill Engine command references (queries, transactions, contracts, script-gen) |
assets/networks.json |
Pharos network config (Atlantic testnet + mainnet) โ engine schema |
assets/tokens.json |
Known token registry (both networks) โ engine schema |
assets/{erc20,airdrop,templates}/ |
Engine assets โ ERC-20 + airdrop Solidity, script-gen templates |
test_sentinel.py |
34 offline, deterministic tests |
skill.json |
Sentinel MCP manifest |
License
MIT-0 (MIT No Attribution) โ free to use, modify, and redistribute. See LICENSE.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.