Discord bot that proves a user controls an ERC-8004 ("Trustless Agents")
registered agent. The user signs an EIP-712 challenge with the wallet
that controls the agent (NFT owner or agentWallet metadata), the bot
recovers the signer, cross-checks against the on-chain Identity Registry,
and records the binding.
Same architecture as the BIP-322 Ordinal Verify Bot, ported to Ethereum.
/verify agent:<eip155:CHAIN:REGISTRY:AGENT_ID>— bot looks upownerOf(agentId)on the configured Identity Registry, builds a single-use EIP-712 payload bound to the user's Discord ID, the agent identifier, a 256-bit nonce, and a 5-minute deadline.- User signs the payload with
eth_signTypedData_v4(from MetaMask, Rabby, WalletConnect, etc.). A staticsign.htmlpage can be hosted on GitHub Pages to make this one click on mobile. /submit signature:<0x…>— bot recovers the signer and accepts only if it matchesownerOf(agentId)orgetMetadata(agentId, "agentWallet").- The verified binding is written to SQLite; configured holder-rules are evaluated against the agent's registration JSON, and matching Discord roles are granted.
All bot replies are ephemeral — signatures, addresses, and agent IDs are never visible to other channel members.
Admins can grant Discord roles automatically based on facts about the agent's registration:
| Match type | Value | Matches when… |
|---|---|---|
registry |
chainId:registryAddress |
Agent is registered in that Identity Registry |
trust_model |
tee-attestation, zkml, etc. |
Agent's supportedTrustModels contains the value |
agent_type |
The agent's type field |
Exact match (case-sensitive) |
service_proto |
mcp, a2a, ens, did, email, … |
Agent advertises a service with that protocol |
Rules are evaluated on every successful /submit and on /sync_me /
/sync_roles. Sync is safe: roles outside the configured rules are never
touched, and a transient RPC error aborts the diff instead of stripping
roles.
| Command | What it does |
|---|---|
/start_verify |
Mobile-friendly walkthrough: three buttons reveal copy-ready prompts for asking your agent for its ID and templates for /verify and /submit |
/register_help |
One-button copy-paste prompt to send to your agent so it self-registers on the configured Identity Registry, then reports back the canonical eip155:… identifier |
/verify agent:<id> |
Issue an EIP-712 challenge for an agent ID. Reply has a 🤖 button that emits an agent-addressed prompt with the typed data inlined — paste it to a wallet-capable AI agent and get a signature back |
/submit signature:<0x…> |
Submit the signature; record the binding |
/whoami |
Flat list of the agents you've verified |
/my_agents |
Per-wallet view: groups your verified agents by signing wallet, flags transferred-away tokens, and surfaces unverified-but-owned agents via on-chain balanceOf |
/sync_me |
Re-check your agents and update your roles |
/sync_roles [@member] |
Admin: sync one member or every verified member |
/rule_add |
Bind a Discord role to an agent fact |
/rule_list |
Show this server's holder-rules |
/rule_remove rule_id:<n> |
Delete a holder-rule |
/rule_help |
Setup guide for holder-rules |
The bot assumes the user is the operator of an autonomous agent, not the
signer themselves. /start_verify and /register_help produce prompts
addressed to the agent (not to the human) — the human pastes one block,
the agent does the on-chain work and replies with a single deterministic
token the human pastes back. Each step is gated behind a Discord UI button
so mobile users get one focused code block to long-press, instead of a
scroll of inline ``` fences.
Specifically, the /start_verify "ask agent for ID" prompt walks the
agent through balanceOf(yourAddress) on the configured registry,
explains that a registered ERC-8004 agent is an ERC-721 NFT minted by
that contract (so tokenId == agentId), and asks for a YES reply in
canonical eip155:CHAIN:REGISTRY:AGENTID form or a literal NOT_REGISTERED
reply that the human can route into /register_help.
Requires Python 3.10+ (the code uses PEP 604 X | None unions).
git clone <repo>
cd eth-agent-verify-bot
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # then edit
python eth_agent_verify_bot.pyOAuth2 scopes: bot, applications.commands. Bot permissions integer
268782592 covers everything the bot uses (View Channels, Send Messages,
Embed Links, Read Message History, Use External Emojis, Manage Roles).
The Manage Roles permission is the only one that's strictly required —
without it /submit records the binding but can't grant the role.
Role hierarchy: the bot's own role must sit above any role it
grants, or add_roles will surface a Forbidden and the bot reports
"sits below this role." This is a Discord rule, not a bot setting.
Privileged intents: none are required for /verify and /submit
(slash commands carry their own data). Set ENABLE_MEMBERS_INTENT=1
and flip the Server Members toggle in the Developer Portal Bot tab
to make admin /sync_roles (no-arg, all-members form) find every
verified member without each having to interact first.
DISCORD_TOKEN— bot token.GUILD_ID— comma-separated list of guild IDs to register slash commands into. One guild = instant per-guild registration. Multiple guilds = one bot process serves multiple servers, each with its own instant sync. Leave empty for global registration (~1 hour to propagate). Per-guild sync errors (e.g., bot not invited there yet) are logged as warnings and don't abort sync of the other guilds.VERIFIED_ROLE_ID— base role granted on any successful verification. Accepts either a bare role id (applies to every guild) or aguild_id:role_id,guild_id:role_id,…mapping so each guild gets its own verified role.ENABLE_MEMBERS_INTENT— set to1/true/yesto enable the Server Members privileged intent (see "Discord permissions" above). Off by default so the bot doesn't crash on connect if the Developer Portal toggle is also off.CHAIN_ID— chainId of the Identity Registry (default8453, Base).RPC_URL— JSON-RPC endpoint for that chain.REGISTRY_ADDR— deployed Identity Registry address.SIGN_PAGE_URL— optional URL of the static EIP-712 signer page; when set,/verifyshows a "Sign with my wallet" link with the typed data pre-filled.DB_PATH— SQLite path (defaultverifications.db).IPFS_GATEWAY— optional HTTPS gateway foripfs://tokenURIvalues.
- EIP-712 payload binds
discordId,agentId,registry, a 256-bit randomnonce, and adeadline. ThechainIdis in the domain separator — signatures cannot be replayed across chains. - Nonces are single-use and held in memory only; bot restart invalidates every pending challenge.
/submitaccepts the recovered signer only if it equalsownerOfor the on-chainagentWallet. Both are re-read fresh at submit time — a transferred NFT immediately fails the next attempt./sync_*re-checks current ownership and removes only roles granted by holder-rules. Roles granted by other bots stay untouched.
pip install pytest
pytest -m "not network"The default suite mocks the registry boundary — no chain reads. Mark
network-dependent tests with @pytest.mark.network for opt-in live runs.
MIT