The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Agentic Wallet Guardian V3 listing page.
A self-hosted decision engine that sits between an AI agent and blockchain execution. Agents submit a proposed action, Guardian returns an explainable ALLOW / WARN / BLOCK before anything gets signed or broadcast.
It runs on your own infrastructure, using your own policy rules and your own reputation data - see Why self-hosted for why that matters and how this differs from calling a hosted security API directly.
See it decide, live: run the API locally (GUARDIAN_ENABLE_CORS_FOR_BROWSER_DEMO=true uvicorn api.main:app --reload), then open examples/browser-demo.html
in a browser - no build step, no server for the page itself. Every
scenario button sends a real POST /decision to your running instance
and renders the actual response (risk score, every signal that fed into
it, every policy rule that fired) - nothing in the page is scripted or
faked. GUARDIAN_ENABLE_CORS_FOR_BROWSER_DEMO is off by default (see
guardian/config.py) since it's specifically for this local-demo case,
not something to leave on for a real deployment.
There are good hosted alternatives for agent-transaction security (GoPlus's AgentGuard, Blockaid, Chainalysis/TRM for compliance). If you just want a risk score and don't care who sees the query, calling one of those directly is less work than running this. Guardian exists for the cases where that tradeoff doesn't work for you:
data/threat_lists/README.md), not a lookup call to a third party. A
hosted API inherently sees every address and amount you ask it about.guardian/policy/, reviewable and
changeable without waiting on anyone else's product roadmap.GUARDIAN_RATE_LIMIT_PER_MINUTE).The honest tradeoff going the other way: you also take on running it, keeping your local threat lists current, and you don't get a hosted vendor's chain coverage or dedicated threat-research team for free. This is the right choice for teams that specifically need data sovereignty or deep policy customization - not a strict upgrade over every hosted option.
Every data source in steps 2-4 is a small provider interface with a mock
implementation (zero config, zero network calls) and a real one, selected
per-source by environment variable - see .env.example. Switching from
demo mode to a real deployment is a config change, not a code change.
guardian/* is intentionally dependency-free (standard library only,
except where a real provider needs httpx or web3), so the decision
core can be unit-tested, embedded in another service, or ported to a
different web framework without dragging FastAPI along. Only api/
touches pydantic/FastAPI.
This is real, runnable, tested decision infrastructure with real (not mock) data sources available for every signal source - but "available" isn't the same as "flip a switch and trust it blindly." Specifics:
is_contract and tx_count (nonce-based) are
reliable with any JSON-RPC endpoint. Wallet age requires an
archive-capable node and is off by default
(GUARDIAN_RPC_ESTIMATE_AGE=false) - most free public RPC endpoints
don't serve historical state, so this fails closed to "unknown" rather
than guessing.GoPlusTokenDataProvider
needs a contract address - a bare symbol like "PEPE" can't be resolved
and is honestly reported as unverifiable rather than guessed at.BLOCK through the full pipeline) and verified to correctly reflect
delistings, not just additions (Tornado Cash's addresses, removed from
the SDN list in March 2025, are correctly absent). Re-run
scripts/refresh_ofac_list.py periodically - sanctions change in both
directions.malicious_contracts.json / verified_contracts.json still ship
empty on purpose (see data/threat_lists/README.md) - there's no
single authoritative source for "malicious contract" the way OFAC's
list is authoritative for sanctions, so populating these is a judgment
call for whoever operates this instance, not something to seed by
default with unverified entries.RpcSimulationProvider
(GUARDIAN_SIMULATION_PROVIDER=rpc) genuinely dry-runs a transaction via
eth_call/eth_estimateGas against current chain state - a revert comes
back with its actual reason, not a guess, and ERC-20 approve() amounts
are decoded from real calldata instead of inferred. This activates when
the caller supplies raw calldata via intent.metadata["data"], OR - new -
when GUARDIAN_TX_BUILDER=rpc is also set and the intent is a plain
transfer or approve (see next bullet). A swap intent with no
transaction built yet still has nothing to dry-run - Guardian reports
that honestly (simulation_not_attempted) rather than guessing.RpcTransactionBuilder (GUARDIAN_TX_BUILDER=rpc) turns a
semantic transfer/approve intent into real calldata - it fetches the
token's actual decimals() via RPC rather than assuming 18 (a wrong
assumption there would scale the amount by orders of magnitude), and
deliberately has no hardcoded token-address registry: a bare symbol like
"USDC" is refused rather than guessed at, since a wrong address here
wouldn't just be a bad risk signal, it'd be an artifact that could end up
in a real transaction. swap is built against Uniswap V2 Router02 only
(one immutable, well-known contract - function selectors computed
locally via Web3.keccak, not copied from memory) - real
getAmountsOut() on-chain quote, caller-supplied max_slippage_bps
required (never a default, same reasoning as decimals above). bridge
is a genuinely open-ended L2/bridge routing problem in general - dozens
of protocols, wildly different trust models - but this module handles
one well-scoped slice of it: L1 -> L2 deposits through a destination
chain's own official OP Stack bridge (currently: Base and Optimism -
depositETHTo/depositERC20To on L1StandardBridge, both addresses
independently cross-checked - Base against Etherscan's label plus
basehub.org, Optimism against the official
ethereum-optimism/superchain-registry plus a second independent
dev-tool config - before being hardcoded).
L2 -> L1 withdrawals are NOT built - that's a genuinely different, much
slower proof/challenge-window flow, not a variant of the deposit call.
Bridging to anywhere else, or via any non-canonical bridge, returns
None rather than guessing.decision/intent_verification.py compares an agent's declared
approve amount against what the simulated calldata really encodes -
but that comparison needs the token's decimals() to convert between
human units and atomic ones. GUARDIAN_DECIMALS_PROVIDER=rpc
(RpcTokenDecimalsProvider, see
guardian/intelligence/token/decimals.py) fetches that for real via
eth_call, cached forever per (chain, token) since a deployed
contract's decimals() can't change. Left at its null default, a
mismatch this module could otherwise catch degrades to an honest
"cannot verify" WARN instead - same "no silent guessing" rule as
everywhere else in this module, not a gap that got missed.
RpcTransactionBuilder shares this same cache when both are
configured with a real provider, instead of doing its own independent,
uncached lookup for the same token.InMemoryStorage (default, zero setup), SQLiteStorage
(GUARDIAN_STORAGE_BACKEND=sqlite - persists across restarts, no
external infra), or PostgresStorage
(GUARDIAN_STORAGE_BACKEND=postgres + GUARDIAN_POSTGRES_DSN - the
fit for multiple replicas behind a load balancer, where SQLite's
single-writer model becomes the bottleneck; pip install -r requirements-postgres.txt). Tested against a real local Postgres
instance, not mocked - see tests/test_postgres_storage.py. Redis is
still open if you specifically want it; the two-method
MemoryBackend interface is small enough to implement against
anything.BLOCK as a strong
signal, not a guarantee, until that's happened.Everything downstream of a Signal - fusion, policy, reputation,
explanation, the API - does not need to change as any of the above
gets hardened further. That boundary is the actual design contract here.
Zero-config demo mode (mock providers, in-memory storage, no auth):
Or with Docker:
Try the canned scenarios:
Or submit your own intent:
Copy .env.example to .env and adjust:
At minimum for a real deployment: set GUARDIAN_API_KEY (auth is off by
default), GUARDIAN_STORAGE_BACKEND=sqlite (persistence), and whichever
GUARDIAN_*_PROVIDER variables you want pointed at real data instead of
mock - see the comments in .env.example for every option, and
RpcWalletDataProvider/BlockscoutContractDataProvider/
DexScreenerTokenDataProvider/GoPlusContractDataProvider/
GoPlusTokenDataProvider's docstrings for what each one actually
gives you.
If more than one agent shares this deployment, also set
GUARDIAN_AGENT_API_KEYS (format: agent_id:key,agent_id2:key2). A
single GUARDIAN_API_KEY only proves a caller holds a valid key - it
does not prove which agent_id a given request actually came from, since
agent_id is just a field in the request body. Anyone holding the shared
key can submit any agent_id and inherit that agent_id's accumulated
reputation and capability grants. GUARDIAN_AGENT_API_KEYS binds each
agent_id to its own key; GUARDIAN_API_KEY, if also set, keeps working
as a master key that can act as any agent, for admin/testing use. For a
genuinely single-agent deployment, GUARDIAN_API_KEY alone is fine.
For agent frameworks that speak MCP (LangChain, CrewAI, Claude Desktop,
etc.), mcp_server.py exposes the same decision engine as two tools
(evaluate_action, get_agent_history) over stdio - install
requirements-mcp.txt alongside requirements.txt (they resolve into one
environment; see the comment at the top of requirements-mcp.txt) and
point your MCP client at python mcp_server.py.
requirements-chain.txt (web3) is only needed for the RPC-provider
tests; the rest of the suite runs with just requirements.txt. The
guardian/* core has no external dependencies beyond that, so it's also
runnable with:
CI (.github/workflows/ci.yml) runs the full suite on every push/PR
against Python 3.11 and 3.12.
Every decision this service returns — from a single policy check up through the full pipeline — is signed as an OAA (Open Agent Attestation) token: an Ed25519-signed JWT wrapping the decision, the action, and the reason.
Anyone holding the public key can verify a decision offline, without calling back to whatever instance of Guardian issued it — useful for an auditor, a downstream service, or just a record you want to trust later without trusting the server that produced it.
The reference OAA implementation is ~150 lines
(oaa.py/attestation.py upstream) and is shared, unmodified,
across this project and agent-guardrail —
same signing format, same verification path, no per-project fork.
MetaMask Agent Wallet's Guard Mode / Beast Mode apply the same static
spend limits and allowlists to every agent. Guardian is a second,
independent check in front of it: does this specific action look
right for this agent, right now — before the mm CLI is ever
invoked.
skills/guardian-check/ is a standard
Agent Skill — the same open format MetaMask
itself uses for mm (npx skills add MetaMask/agent-skills). Install
it alongside MetaMask's own skill in any Agent-Skills-compatible
runtime (Claude Code, Cursor, Codex, OpenClaw), and the agent will
call a running Guardian instance for an ALLOW/WARN/BLOCK decision
before running any mm command that moves funds — mm send, mm swap, mm bridge, mm perps, mm predict trade, mm earn, mm aave, mm pay.
Guardian never holds keys and never executes anything — mm remains
the only thing that signs or broadcasts. This is a decision gate the
agent is instructed to consult first, not a modification to
MetaMask's own pipeline (there's no public hook for that today).
Everything above is advisory - Guardian tells you ALLOW/WARN/BLOCK, but
the caller still has to choose to respect that. onchain/
is a real ERC-7579 validator module for ERC-4337 smart accounts that
closes that gap: once installed, the account's UserOperations only ever
reach the chain if a trusted Guardian signer attested, for that exact
operation, that the decision was ALLOW - not something an agent can skip
asking or ignore the answer to. See onchain/README.md
for why it needs a second, EVM-native signature format alongside OAA
(Ed25519 has no EVM precompile and costs ~2,000,000 gas to verify in pure
Solidity; this module's entire validateUserOp costs 27k-59k gas), what's
deliberately out of scope (WARN never passes on-chain; no professional
audit yet), and how to build, test, and deploy it - including a test that
verifies a signature produced by real, running Python
(guardian/onchain_attestation.py) is accepted by the real Solidity
contract, not two implementations that only agree with themselves.
transfer/
approve end to end (GUARDIAN_SIMULATION_PROVIDER=rpc +
GUARDIAN_TX_BUILDER=rpc - see
Honesty about the current state).
swap needs real DEX routing.getAmountsOut() quote, explicit caller-supplied
max_slippage_bps (never defaulted), no calldata built without a real
quote. Fixed a real bug found while building this: simulation was
dry-running against intent.target (the recipient/spender encoded
inside ERC-20 calldata) instead of the actual contract being called
(from_token) - meaning transfer/approve simulation silently
"succeeded" against any EOA recipient regardless of whether the real
call would have reverted. BuiltTransaction now carries an explicit
to; see tests/test_tx_builder.py for the regression tests that
would have caught it. bridge still open.L1StandardBridge (depositETHTo/depositERC20To) - other
destinations, other bridge protocols, and L2->L1 withdrawals all
remain open; see
Honesty about the current state.sanctioned_addresses.json - 103 real OFAC
SDN addresses, refreshable via scripts/refresh_ofac_list.py).
malicious_contracts.json / verified_contracts.json remain empty by
design - no single authoritative source exists to seed them the way
OFAC's list does for sanctions.InMemoryStorage for a persistent backend.SQLiteStorage is
available; PostgresStorage done - tested against a real local
Postgres instance (tests/test_postgres_storage.py), same
two-method MemoryBackend interface as the other backends. Redis
remains open if specifically wanted.mcp_server.py). A packaged
Python/TypeScript SDK on top of the REST API is still open.BLOCK from this service in production - it's a security
tool, so it needs the same scrutiny it applies to others.guardian/policy/capabilities.py, and wired into
DecisionEngine.evaluate() via an optional capability_registry
constructor argument (previously it was a standalone module you had
to call yourself outside the normal pipeline - see
examples/example_capability_limits.py, which now runs through
DecisionEngine directly). Opt-in: pass no registry (the default)
and nothing changes; an operator can grant a specific agent a scoped
capability (allowed action types, allowed chains, per-action and
daily spending caps, an expiry) with zero private-key material
involved. Agents with no grant are unaffected. This module still
never touches private keys or session-key issuance itself - that
remains out of scope, a categorically higher-stakes problem. onchain/'s GuardianValidator is an ERC-7579 module
that makes a decision genuinely unbypassable for any ERC-4337 smart
account that installs it, without Guardian ever holding a key. It
does not manage session keys, custody, or account creation - it
only gates execution behind an attestation - so this is a real,
load-bearing piece of "account abstraction," not the whole of it.guardian/decision/intent_verification.py catches
the case where an agent declares one amount but the actual calldata
it was handed encodes a meaningfully different (but still finite)
one, and DecisionEngine.evaluate() now actually calls it (it
didn't before - the module and its example script existed, but
nothing in the real decision pipeline invoked it). decimals(), and no decimals provider exists yet.GUARDIAN_DECIMALS_PROVIDER=rpc (RpcTokenDecimalsProvider, see
guardian/intelligence/token/decimals.py) closes that: a real
eth_call to the token's own decimals(), cached forever per
(chain, token) since that value can never change once a contract is
deployed. Left at its null default, every approve with a
successful, finite-amount simulation still gets an honest "cannot
verify without decimals" WARN instead of either a false BLOCK or a
silent skip - configuring the real provider is what turns that into
an actual BLOCK on a genuine mismatch. See
examples/example_intent_verification.py for the check blocking a
real mismatch, and tests/test_decision_engine.py's
TestIntentVerificationWithRealDecimalsProvider for the end-to-end
version wired through a real (mocked-RPC) decimals lookup rather
than a hand-supplied token_decimals argument.guardian/intelligence/anomaly/analyzer.py.
Distinct from reputation (a single trust score) and policy (static,
operator-set limits): this compares the current intent against
this specific agent's own recorded history - new action type,
new chain, or an amount that's a statistical outlier versus what
this agent has done before, even if it's within policy limits and
the agent's reputation is fine. Honestly reports "insufficient
history" rather than guessing a baseline from fewer than 5 prior
data points - see tests/test_anomaly_detection.py.skills/guardian-check/ is a standard Agent Skill an agent
installs alongside MetaMask's own mm skill; the agent calls it
before running any fund-moving mm command and only proceeds on
ALLOW. Tested end-to-end against a live uvicorn instance
(ALLOW/WARN/BLOCK/config-error all exercised for real, not just
asserted) - see the "Using Guardian in front of MetaMask Agent
Wallet" section above. No public hook exists (yet) to run inside
MetaMask's own pipeline; this works at the agent-orchestration
layer instead.Same author, same principle applied elsewhere:
MIT - see LICENSE.