Skip to main content
Glama
mattcrypted
by mattcrypted

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) plus sentinel.md, the risk-gate reference.

  • assets/ โ€” the engine's networks.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.

Related MCP server: aegis-defi

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 cast CLI โ€” 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_plan returns 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. approve to 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).

The score is additive, so signals stack; the verdict is a band over it (>=70 dangerous, >=35 caution, else safe). Every signal feeds one additive score, which a band turns into the verdict and execution_plan turns 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(["additive score"])
    px --> sc
    bc --> sc
    own --> sc
    tok --> sc
    st --> sc
    sc --> band{"score band"}
    band -->|">= 70"| dg["dangerous"]
    band -->|">= 35"| ca["caution"]
    band -->|"else"| 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": 35,
  "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 ยท explorer https://atlantic.pharosscan.xyz ยท gas token PHRS

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 (20)

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 โ€” below the caution line 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 (25)

latent SELFDESTRUCT

After (pause tx)

true

๐ŸŸก caution (45)

+ 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 Backdoor fixture โ†’ 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.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

โ€“Maintainers
โ€“Response time
โ€“Release cycle
โ€“Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mattcrypted/pharos-sentinel'

If you have feedback or need assistance with the MCP directory API, please join our Discord server