
@2004/Erc 8004
- 3 installs
- 1 repo stars
- Updated July 29, 2026
- starchild-ai-agent/community-skills
Helps with ai & agent building tasks.
About
@2004/erc-8004 is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- @2004/erc-8004
- AI & Agent Building
- AI-coding skill
@2004/Erc 8004 by the numbers
- 3 all-time installs (skills.sh)
- Ranked #13,657 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/starchild-ai-agent/community-skills --skill erc-8004Add your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 29, 2026 |
| Repository | starchild-ai-agent/community-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
🪪 ERC-8004 Skill
Community edition — updated for Starchild skill marketplace.
Implementation of the ERC-8004 Trustless Agents standard — the Ethereum Foundation dAI team's on-chain Identity + Reputation + Validation layer for AI agents.
- Spec: https://eips.ethereum.org/EIPS/eip-8004
- Contracts: https://github.com/erc-8004/erc-8004-contracts
- This skill gives Starchild agents a portable, censorship-resistant on-chain identity
and a public reputation track record that any other agent can query.
What's in this skill
| Layer | Status |
|---|---|
| Identity Registry (ERC-721 agent identity) | ✅ Full read + write |
| Reputation Registry (feedback + summary) | ✅ Full read + write |
| Validation Registry | ⏸ Skipped — official README marks it as under active TEE-community revision |
Tx broadcasting goes through the Starchild wallet skill backend (tools.wallet._wallet_request → POST /agent/transfer), so the agent's Privy wallet signs and gas is platform-sponsored when available.
Supported chains
Singleton deployments per chain (same address triple across testnets, separate triple across mainnets):
| Chain | chainId | Identity | Reputation |
|---|---|---|---|
| base-sepolia (default) | 84532 | 0x8004A818…BD9e | 0x8004B663…8713 |
| base | 8453 | 0x8004A169…a432 | 0x8004BAa1…9b63 |
| ethereum | 1 | same as base | same as base |
| ethereum-sepolia | 11155111 | same as base-sepolia | same as base-sepolia |
Pass chain="base" etc. to any function. Default is base-sepolia.
Usage from scripts
from core.skill_tools import erc_8004
# 1. Register this agent
r = erc_8004.register_agent(
name="Crypto Research Agent",
description="On-chain analyst — funding rates, OI, social sentiment, on demand.",
services=[
{"name": "web", "endpoint": "https://my-agent.com/"},
{"name": "A2A", "endpoint": "https://my-agent.com/.well-known/agent-card.json", "version": "0.3.0"},
],
x402_support=True,
supported_trust=["reputation"],
)
print(r["agent_id"], r["explorer_url"], r["nft_url"])
# 2. Discover other agents
others = erc_8004.discover_agents(limit=20, filter_tag="reputation")
# 3. Fetch one agent's full registration
agent = erc_8004.get_agent(agent_id=42)
print(agent["registration_file"]["name"])
# 4. Leave feedback (caller MUST NOT be the agent's owner)
erc_8004.give_feedback(
agent_id=42,
value=5, # 5/5 stars
tag1="rating",
endpoint="https://my-agent.com/research",
feedback_uri="ipfs://Qm.../review.json",
)
# 5. Aggregate reputation (auto-fetches all reviewers if not specified)
rep = erc_8004.get_reputation(agent_id=42)
print(rep["count"], rep["avg"])
# 6. Pull individual feedback entries
for f in erc_8004.list_feedback(agent_id=42):
print(f["client_address"], f["human_value"], f["tag1"])Key semantics & gotchas
- agentId is an ERC-721 tokenId, assigned incrementally per chain. Globally unique
identifier: eip155:{chainId}:{identityRegistryAddress}:{agentId}.
- Registration file is the off-chain JSON pointed to by
tokenURI. By default
this skill encodes it as a data:application/json;base64,… URI — fully on-chain, no IPFS / HTTPS hosting required.
- Sybil mitigation:
getSummaryrequires non-emptyclient_addresses. Pass the
reviewer set you trust. get_reputation(...) defaults to "all reviewers ever" if you don't specify, which is convenient but not Sybil-resistant — pick a reviewer allowlist for high-stakes decisions.
- Owner cannot self-rate: the Reputation Registry reverts if
msg.senderis the
agent's owner or operator. Use a separate wallet for testing.
- Validation Registry skipped: official README marks it as still under revision
with the TEE community. Will be added in a follow-up.
Gas + tx-hash resolution (read this before debugging "no tx_hash")
Calls go through wallet_transfer which uses platform gas sponsorship by default (verified working on Base Sepolia via Alchemy paymaster, ERC-4337 EntryPoint v0.7 at 0x0000000071727De22E5E9d8BAf0edAc6f37da032). Sponsored txs come back from the wallet backend as:
{"data": {"hash": "", "user_operation_hash": "0x…", "sponsorship_provider": "alchemy", ...}}The skill resolves user_operation_hash → real tx_hash by scanning EntryPoint `UserOperationEvent` logs — not by polling a public bundler. This is critical: Alchemy / Pimlico / Stackup / Biconomy don't share bundler mempools, so a Pimlico eth_getUserOperationReceipt for an Alchemy-submitted op will return null forever. Log scan is bundler-agnostic. See _resolve_user_op_hash in _utils.py.
If you ever see No tx_hash in wallet response, it means the EntryPoint scan timed out (default 90s, scanning latest 500 blocks) — increase timeout= on send_contract_tx or widen lookback_blocks.
If gas sponsorship is unavailable on the chain you target, fund the agent wallet from the corresponding faucet first (Base Sepolia: coinbase / alchemy / quicknode).
Demo: end-to-end agent commerce
See output/eth-hk-demo/ for a full scripted demo combining this skill with:
- ERC-8183 (Agentic Commerce) for escrow + evaluator-attested completion
- x402 for HTTP-native USDC micropayments
Changelog (v0.2.0)
- Bumped for community marketplace publish
- Added "Community edition" header
- Minor doc cleanup for discoverability
"""
ERC-8004 Identity Registry operations.
Spec: https://eips.ethereum.org/EIPS/eip-8004
The Identity Registry is an ERC-721 where each tokenId == agentId.
tokenURI points to an off-chain JSON "agent registration file".
"""
from __future__ import annotations
import json
from typing import Any
from web3 import Web3
from _utils import (
chain_config,
explorer_token,
get_contract,
get_w3,
send_contract_tx,
wallet_address,
)
# ── Registration file helpers ────────────────────────────────────────────────
def build_registration_file(
name: str,
description: str,
services: list[dict] | None = None,
*,
image: str = "",
x402_support: bool = False,
supported_trust: list[str] | None = None,
chain: str | None = None,
agent_id: int | None = None,
) -> dict[str, Any]:
"""
Build a spec-compliant registration file.
`services` is a list of dicts like:
{"name": "A2A", "endpoint": "https://...", "version": "0.3.0"}
{"name": "MCP", "endpoint": "https://..."}
{"name": "web", "endpoint": "https://..."}
"""
cfg = chain_config(chain)
registrations = []
if agent_id is not None:
registrations.append({
"agentId": agent_id,
"agentRegistry": f"eip155:{cfg['chain_id']}:{cfg['identity_registry']}",
})
return {
"type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
"name": name,
"description": description,
"image": image,
"services": services or [],
"x402Support": x402_support,
"active": True,
"registrations": registrations,
"supportedTrust": supported_trust or ["reputation"],
}
def to_data_uri(reg_file: dict) -> str:
"""Encode the registration file as a `data:application/json;base64,...` URI.
Useful for fully on-chain registration when no IPFS / HTTPS host is handy.
"""
import base64
raw = json.dumps(reg_file, separators=(",", ":")).encode()
b64 = base64.b64encode(raw).decode()
return f"data:application/json;base64,{b64}"
def decode_data_uri(uri: str) -> dict | None:
if not uri.startswith("data:application/json;base64,"):
return None
import base64
b64 = uri.split(",", 1)[1]
return json.loads(base64.b64decode(b64).decode())
# ── On-chain operations ──────────────────────────────────────────────────────
def register_agent(
agent_uri: str,
*,
chain: str | None = None,
metadata: list[dict] | None = None,
wait: bool = True,
) -> dict[str, Any]:
"""
Register a new agent. Returns {agent_id, tx_hash, explorer_url, ...}.
`metadata` is an OPTIONAL list of {"key": str, "value": bytes}.
Note: the reserved `agentWallet` key is set automatically to the owner.
"""
contract = get_contract("IdentityRegistry", chain)
if metadata:
meta_struct = [(m["key"], m["value"]) for m in metadata]
result = send_contract_tx(
contract, "register", [agent_uri, meta_struct],
chain=chain, wait=wait,
)
else:
result = send_contract_tx(
contract, "register", [agent_uri],
chain=chain, wait=wait,
)
# Parse Registered event for agentId
agent_id = None
if result.get("receipt"):
registered_topic = Web3.keccak(text="Registered(uint256,string,address)").hex()
for log in result.get("logs", []):
if (
log["address"].lower() == contract.address.lower()
and len(log["topics"]) >= 3
and log["topics"][0] == registered_topic
):
agent_id = int(log["topics"][1], 16)
break
result["agent_id"] = agent_id
if agent_id is not None:
result["nft_url"] = explorer_token(chain, contract.address, agent_id)
return result
def set_agent_uri(agent_id: int, new_uri: str, *, chain: str | None = None, wait: bool = True) -> dict:
contract = get_contract("IdentityRegistry", chain)
return send_contract_tx(
contract, "setAgentURI", [agent_id, new_uri],
chain=chain, wait=wait,
)
def get_agent(agent_id: int, *, chain: str | None = None) -> dict[str, Any]:
"""Fetch agent on-chain metadata + parsed registration file if reachable."""
contract = get_contract("IdentityRegistry", chain)
try:
token_uri = contract.functions.tokenURI(agent_id).call()
except Exception as e:
raise ValueError(f"Agent {agent_id} not found on {chain or 'default'}: {e}")
owner = contract.functions.ownerOf(agent_id).call()
agent_wallet = contract.functions.getAgentWallet(agent_id).call()
out: dict[str, Any] = {
"agent_id": agent_id,
"token_uri": token_uri,
"owner": owner,
"agent_wallet": agent_wallet,
"registration_file": None,
"fetch_error": None,
}
# Try to resolve the registration file
reg = None
try:
if token_uri.startswith("data:application/json;base64,"):
reg = decode_data_uri(token_uri)
elif token_uri.startswith("ipfs://"):
cid = token_uri[len("ipfs://"):]
from urllib.request import urlopen
with urlopen(f"https://ipfs.io/ipfs/{cid}", timeout=10) as r:
reg = json.loads(r.read())
elif token_uri.startswith(("http://", "https://")):
from urllib.request import urlopen
with urlopen(token_uri, timeout=10) as r:
reg = json.loads(r.read())
except Exception as e:
out["fetch_error"] = str(e)
out["registration_file"] = reg
return out
def get_metadata(agent_id: int, key: str, *, chain: str | None = None) -> bytes:
return get_contract("IdentityRegistry", chain).functions.getMetadata(agent_id, key).call()
def total_supply_estimate(*, chain: str | None = None) -> int:
"""
ERC-8004 IdentityRegistry doesn't expose totalSupply by default — we estimate
by binary-searching for the highest valid tokenId. Cached upstream is recommended.
"""
contract = get_contract("IdentityRegistry", chain)
lo, hi = 0, 1
# find a hi that's not minted
while True:
try:
contract.functions.ownerOf(hi).call()
lo = hi
hi *= 2
if hi > 2**24:
break
except Exception:
break
# binary search
while lo + 1 < hi:
mid = (lo + hi) // 2
try:
contract.functions.ownerOf(mid).call()
lo = mid
except Exception:
hi = mid
return lo
def discover_agents(
*,
chain: str | None = None,
limit: int = 25,
from_id: int = 0,
include_registration: bool = True,
max_scan: int | None = None,
max_consecutive_gaps: int = 200,
) -> list[dict[str, Any]]:
"""
Enumerate agents starting at `from_id`, scanning tokenIds until either
`limit` agents are found, we hit `max_consecutive_gaps` unminted tokenIds
in a row (presumed end of registry), or we scan `max_scan` ids total.
NOTE: ERC-8004 IdentityRegistry has no totalSupply. For production-scale
discovery use Transfer event indexing or a subgraph; this is a simple
bounded scan suitable for demos and small registries.
"""
out: list[dict[str, Any]] = []
contract = get_contract("IdentityRegistry", chain)
consecutive_gaps = 0
scanned = 0
aid = from_id
while len(out) < limit:
if max_scan is not None and scanned >= max_scan:
break
try:
owner = contract.functions.ownerOf(aid).call()
consecutive_gaps = 0
if include_registration:
try:
agent = get_agent(aid, chain=chain)
except Exception:
agent = {"agent_id": aid, "owner": owner}
else:
agent = {"agent_id": aid, "owner": owner}
out.append(agent)
except Exception:
consecutive_gaps += 1
if consecutive_gaps >= max_consecutive_gaps:
break
aid += 1
scanned += 1
return out
"""
ERC-8004 Reputation Registry operations.
Spec: https://eips.ethereum.org/EIPS/eip-8004
Feedback is `int128 value` + `uint8 valueDecimals`, plus optional tags / endpoint /
off-chain feedbackURI. Per spec, getSummary REQUIRES non-empty clientAddresses
to mitigate Sybil — caller decides which reviewers to trust.
"""
from __future__ import annotations
from typing import Any
from web3 import Web3
from _utils import get_contract, send_contract_tx
def give_feedback(
agent_id: int,
value: int,
*,
value_decimals: int = 0,
tag1: str = "",
tag2: str = "",
endpoint: str = "",
feedback_uri: str = "",
feedback_hash: bytes | str = b"\x00" * 32,
chain: str | None = None,
wait: bool = True,
) -> dict[str, Any]:
"""
Submit feedback for an agent. `value` is the raw fixed-point integer:
e.g. 5-star rating: value=5, value_decimals=0
87/100: value=87, value_decimals=0
99.77% uptime: value=9977, value_decimals=2
Caller cannot be the agent's owner / operator (enforced by contract).
"""
if isinstance(feedback_hash, str):
if feedback_hash.startswith("0x"):
feedback_hash = bytes.fromhex(feedback_hash[2:])
else:
feedback_hash = feedback_hash.encode()[:32].ljust(32, b"\x00")
if len(feedback_hash) != 32:
raise ValueError("feedback_hash must be exactly 32 bytes")
contract = get_contract("ReputationRegistry", chain)
return send_contract_tx(
contract,
"giveFeedback",
[agent_id, int(value), int(value_decimals), tag1, tag2, endpoint, feedback_uri, feedback_hash],
chain=chain,
wait=wait,
)
def revoke_feedback(
agent_id: int,
feedback_index: int,
*,
chain: str | None = None,
wait: bool = True,
) -> dict[str, Any]:
contract = get_contract("ReputationRegistry", chain)
return send_contract_tx(
contract, "revokeFeedback", [agent_id, int(feedback_index)],
chain=chain, wait=wait,
)
def append_response(
agent_id: int,
client_address: str,
feedback_index: int,
response_uri: str,
response_hash: bytes | str = b"\x00" * 32,
*,
chain: str | None = None,
wait: bool = True,
) -> dict[str, Any]:
if isinstance(response_hash, str):
if response_hash.startswith("0x"):
response_hash = bytes.fromhex(response_hash[2:])
else:
response_hash = response_hash.encode()[:32].ljust(32, b"\x00")
contract = get_contract("ReputationRegistry", chain)
return send_contract_tx(
contract, "appendResponse",
[agent_id, Web3.to_checksum_address(client_address), int(feedback_index), response_uri, response_hash],
chain=chain, wait=wait,
)
def get_summary(
agent_id: int,
client_addresses: list[str],
*,
tag1: str = "",
tag2: str = "",
chain: str | None = None,
) -> dict[str, Any]:
"""
Returns {count, summary_value, summary_value_decimals, avg}.
Per spec, client_addresses MUST be non-empty. Pick reviewers you trust.
"""
if not client_addresses:
raise ValueError("client_addresses must be non-empty (Sybil mitigation per ERC-8004)")
contract = get_contract("ReputationRegistry", chain)
addrs = [Web3.to_checksum_address(a) for a in client_addresses]
count, sval, sdec = contract.functions.getSummary(agent_id, addrs, tag1, tag2).call()
# avg is 0.0 (not None) when count==0 — keeps the field numeric so callers
# can f-string format it without a None check.
avg = (sval / count) / (10 ** sdec) if count > 0 else 0.0
return {
"count": count,
"summary_value": sval,
"summary_value_decimals": sdec,
"avg": avg,
}
def read_all_feedback(
agent_id: int,
*,
client_addresses: list[str] | None = None,
tag1: str = "",
tag2: str = "",
include_revoked: bool = False,
chain: str | None = None,
) -> list[dict[str, Any]]:
contract = get_contract("ReputationRegistry", chain)
addrs = [Web3.to_checksum_address(a) for a in (client_addresses or [])]
clients, indexes, values, decimals, tag1s, tag2s, revoked = contract.functions.readAllFeedback(
agent_id, addrs, tag1, tag2, include_revoked
).call()
out = []
for i in range(len(clients)):
dec = decimals[i]
out.append({
"client_address": clients[i],
"feedback_index": indexes[i],
"value": values[i],
"value_decimals": dec,
"human_value": values[i] / (10 ** dec) if dec else values[i],
"tag1": tag1s[i],
"tag2": tag2s[i],
"revoked": revoked[i],
})
return out
def get_clients(agent_id: int, *, chain: str | None = None) -> list[str]:
return get_contract("ReputationRegistry", chain).functions.getClients(agent_id).call()
def get_last_index(agent_id: int, client_address: str, *, chain: str | None = None) -> int:
return (
get_contract("ReputationRegistry", chain)
.functions.getLastIndex(agent_id, Web3.to_checksum_address(client_address))
.call()
)
def read_feedback(
agent_id: int,
client_address: str,
feedback_index: int,
*,
chain: str | None = None,
) -> dict[str, Any]:
value, decimals, tag1, tag2, revoked = (
get_contract("ReputationRegistry", chain)
.functions.readFeedback(agent_id, Web3.to_checksum_address(client_address), feedback_index)
.call()
)
return {
"value": value,
"value_decimals": decimals,
"human_value": value / (10 ** decimals) if decimals else value,
"tag1": tag1,
"tag2": tag2,
"revoked": revoked,
}
"""
ERC-8004 utilities: web3 client, ABI loading, chain config, tx broadcast.
Broadcasting goes through the Starchild wallet skill backend
(`wallet_transfer` POST /agent/transfer) so the platform-managed Privy
wallet signs and pays gas (sponsored when available, user-funded otherwise).
"""
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
from typing import Any
# ---------------------------------------------------------------------------
# Workaround for skill_tools loader: it deletes web3/eth_hash from sys.modules
# between skill loads, which causes duplicate `BackendAPI` class objects to
# accumulate. When eth_utils.crypto.keccak (bound at its module init) later
# routes through a stale eth_hash.auto.Keccak256 instance, the lazy
# `_initialize()` calls `auto_choose_backend()` which `isinstance`-checks the
# pycryptodome backend against the *latest* `BackendAPI` class — and fails.
#
# Fix: explicitly bind a stable pycryptodome backend to the current
# eth_hash.auto.keccak singleton BEFORE web3 imports run. This makes the
# hasher purely data-driven (no further class checks), surviving sys.modules
# churn between skill loads.
# ---------------------------------------------------------------------------
try:
import eth_hash.auto as _eh_auto
from eth_hash.backends.pycryptodome import backend as _eh_pcd_backend
_eh_auto.keccak.hasher = _eh_pcd_backend.keccak256
_eh_auto.keccak.preimage = _eh_pcd_backend.preimage
except Exception:
pass # if it fails we'll get the original error later — surface, don't hide
from web3 import Web3
from web3.contract import Contract
_HERE = Path(__file__).resolve().parent
_CONTRACTS = _HERE / "contracts"
_ADDRESSES: dict[str, Any] | None = None
_ABI_CACHE: dict[str, list] = {}
_W3_CACHE: dict[str, Web3] = {}
def load_addresses() -> dict[str, Any]:
global _ADDRESSES
if _ADDRESSES is None:
_ADDRESSES = json.loads((_CONTRACTS / "addresses.json").read_text())
return _ADDRESSES
def chain_config(chain: str | None = None) -> dict[str, Any]:
addrs = load_addresses()
if chain is None:
chain = addrs["default_chain"]
if chain not in addrs["chains"]:
raise ValueError(f"Unknown chain '{chain}'. Known: {list(addrs['chains'].keys())}")
cfg = dict(addrs["chains"][chain])
cfg["name"] = chain
return cfg
def _live_web3_cls():
"""Always fetch the *current* Web3 class from sys.modules, not the one
captured at skill-load time. The skill loader's sys.modules cleanup can
leave us holding a stale Web3 class whose .eth namespace has a broken
parent-reference descriptor. Re-import per call to stay in sync."""
import importlib
import web3 as _w3pkg
importlib.reload(_w3pkg) if _w3pkg.Web3 is not Web3 else None # noqa
return _w3pkg.Web3
def get_w3(chain: str | None = None):
"""Build a fresh Web3 each call — caching across skill-loader sys.modules
churn can leave instances pointing at stale class hierarchies."""
cfg = chain_config(chain)
name = cfg["name"]
rpc = os.environ.get(f"RPC_{name.upper().replace('-', '_')}", cfg["rpc"])
W3 = _live_web3_cls()
return W3(W3.HTTPProvider(rpc, request_kwargs={"timeout": 30}))
def load_abi(name: str) -> list:
"""name in {IdentityRegistry, ReputationRegistry, ValidationRegistry}"""
if name not in _ABI_CACHE:
raw = json.loads((_CONTRACTS / "abis" / f"{name}.json").read_text())
_ABI_CACHE[name] = raw if isinstance(raw, list) else raw.get("abi", [])
return _ABI_CACHE[name]
def get_contract(name: str, chain: str | None = None):
cfg = chain_config(chain)
addr_key = {
"IdentityRegistry": "identity_registry",
"ReputationRegistry": "reputation_registry",
"ValidationRegistry": "validation_registry",
}[name]
addr = cfg.get(addr_key)
if not addr:
raise RuntimeError(
f"{name} not deployed on '{cfg['name']}'. "
f"Note: ValidationRegistry is under active TEE-community revision."
)
W3 = _live_web3_cls()
w3 = get_w3(chain)
return w3.eth.contract(address=W3.to_checksum_address(addr), abi=load_abi(name))
def build_calldata(contract: Contract, fn_name: str, args: list) -> str:
"""Encode a function call into hex calldata (0x-prefixed).
Supports overloaded functions: pass a Solidity-style signature like
'register(string)' or 'register(string,(string,bytes)[])' as fn_name to
pick a specific overload. Plain names work when unambiguous.
"""
if "(" in fn_name:
fn = contract.get_function_by_signature(fn_name)
else:
try:
fn = contract.get_function_by_name(fn_name)
except Exception:
# Fallback: try to disambiguate by arg count
candidates = [
f for f in contract.all_functions()
if f.abi.get("name") == fn_name and len(f.abi.get("inputs", [])) == len(args)
]
if len(candidates) == 1:
fn = candidates[0]
else:
raise
return fn(*args)._encode_transaction_data()
def explorer_tx(chain: str | None, tx_hash: str) -> str:
cfg = chain_config(chain)
return f"{cfg['explorer']}/tx/{tx_hash}"
def explorer_address(chain: str | None, addr: str) -> str:
cfg = chain_config(chain)
return f"{cfg['explorer']}/address/{addr}"
def explorer_token(chain: str | None, addr: str, token_id: int) -> str:
cfg = chain_config(chain)
return f"{cfg['explorer']}/token/{addr}?a={token_id}"
# ── ERC-4337 user-op hash → real tx hash resolver ───────────────────────────
# EntryPoint addresses (same across all EVM chains — singleton deployments)
_ENTRY_POINT_V07 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"
_ENTRY_POINT_V06 = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
# UserOperationEvent topic (identical signature in v0.6 + v0.7)
_USER_OP_EVENT_TOPIC = (
"0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f"
)
def _resolve_user_op_hash(
user_op_hash: str,
*,
chain: str | None = None,
timeout: float = 90.0,
lookback_blocks: int = 500,
) -> str | None:
"""Resolve a sponsored ERC-4337 user_op_hash to the real on-chain tx_hash
by scanning EntryPoint UserOperationEvent logs. Bundler-agnostic (works
with Alchemy / Pimlico / Stackup / Biconomy — none of which share mempools).
Polls until found or `timeout` seconds elapse.
"""
import time
W3 = _live_web3_cls()
w3 = get_w3(chain)
# Normalize the topic — must be 0x-prefixed lowercase 32 bytes
op_hash = user_op_hash.lower()
if not op_hash.startswith("0x"):
op_hash = "0x" + op_hash
deadline = time.time() + timeout
last_scanned_to = None
while time.time() < deadline:
try:
latest = w3.eth.block_number
from_block = max(0, latest - lookback_blocks)
for ep in (_ENTRY_POINT_V07, _ENTRY_POINT_V06):
logs = w3.eth.get_logs({
"fromBlock": from_block,
"toBlock": latest,
"address": W3.to_checksum_address(ep),
"topics": [_USER_OP_EVENT_TOPIC, op_hash],
})
if logs:
tx = logs[0]["transactionHash"]
return tx.hex() if hasattr(tx, "hex") else tx
last_scanned_to = latest
except Exception:
pass
time.sleep(3.0)
return None
# ─────────────────────────────────────────────────────────────────────────────
# Broadcasting via Starchild wallet skill
# ─────────────────────────────────────────────────────────────────────────────
def wallet_address() -> str:
"""Return the agent's primary EVM wallet address."""
try:
from tools.wallet import _wallet_request
import asyncio
try:
loop = asyncio.get_event_loop()
info = loop.run_until_complete(_wallet_request("GET", "/agent/wallet"))
except RuntimeError:
info = asyncio.run(_wallet_request("GET", "/agent/wallet"))
# Response shape: {"wallets": [{"chain_type": "ethereum", "wallet_address": "0x..."}, ...]}
for w in info.get("wallets", []):
if w.get("chain_type") == "ethereum":
W3 = _live_web3_cls()
return W3.to_checksum_address(w.get("wallet_address") or w.get("address"))
raise RuntimeError(f"No EVM wallet in response: {info}")
except ImportError:
# Outside Fly machine — fall back to env override (testing only)
addr = os.environ.get("AGENT_EVM_ADDRESS")
if not addr:
raise RuntimeError("Cannot resolve agent wallet (no tools.wallet, no AGENT_EVM_ADDRESS)")
return Web3.to_checksum_address(addr)
def send_contract_tx(
contract: Contract,
fn_name: str,
args: list,
*,
chain: str | None = None,
value: int = 0,
gas_limit: int | None = None,
sponsor: bool | None = None,
wait: bool = True,
poll_interval: float = 2.0,
timeout: float = 90.0,
) -> dict[str, Any]:
"""
Encode + broadcast a contract call via the agent's Privy wallet.
Returns: {tx_hash, explorer_url, status, receipt?}
"""
from tools.wallet import _wallet_request
import asyncio
import time
cfg = chain_config(chain)
data = build_calldata(contract, fn_name, args)
body = {
"to": contract.address,
"amount": str(value),
"chain_id": cfg["chain_id"],
"data": data,
}
if gas_limit is not None:
body["gas_limit"] = str(gas_limit)
if sponsor is not None:
body["sponsor"] = sponsor
try:
loop = asyncio.get_event_loop()
resp = loop.run_until_complete(_wallet_request("POST", "/agent/transfer", body))
except RuntimeError:
resp = asyncio.run(_wallet_request("POST", "/agent/transfer", body))
# The wallet API returns either:
# (a) {"hash": "0x...", ...} (eth_sendRawTransaction path, no sponsorship)
# (b) {"data": {"hash": "", "user_operation_hash": "0x...",
# "sponsorship_provider": "alchemy", ...}} (ERC-4337 path)
data = resp.get("data", resp)
tx_hash = (
data.get("tx_hash") or data.get("hash")
or data.get("transaction_hash")
or resp.get("tx_hash") or resp.get("hash") or resp.get("transaction_hash")
)
user_op_hash = data.get("user_operation_hash") or resp.get("user_operation_hash")
# If we got a sponsored user-op back, resolve it to the real tx_hash via
# a public ERC-4337 bundler (Pimlico for Base Sepolia).
if not tx_hash and user_op_hash:
tx_hash = _resolve_user_op_hash(user_op_hash, chain=chain, timeout=timeout)
if not tx_hash:
raise RuntimeError(
f"No tx_hash in wallet response and EntryPoint log scan timed out "
f"after {timeout}s. user_op_hash={user_op_hash} raw={resp}"
)
# Normalize hex bytes → 0x-prefixed string
if isinstance(tx_hash, (bytes, bytearray)):
tx_hash = "0x" + tx_hash.hex()
elif hasattr(tx_hash, "hex"):
tx_hash = tx_hash.hex()
if not tx_hash.startswith("0x"):
tx_hash = "0x" + tx_hash
out = {
"tx_hash": tx_hash,
"explorer_url": explorer_tx(chain, tx_hash),
"status": "pending",
"raw_response": resp,
}
if wait:
w3 = get_w3(chain)
deadline = time.time() + timeout
while time.time() < deadline:
try:
receipt = w3.eth.get_transaction_receipt(tx_hash)
out["status"] = "success" if receipt.status == 1 else "failed"
out["receipt"] = dict(receipt)
# decode log topics for caller convenience
out["logs"] = [
{
"address": log["address"],
"topics": [t.hex() for t in log["topics"]],
"data": log["data"].hex() if isinstance(log["data"], bytes) else log["data"],
}
for log in receipt.logs
]
return out
except Exception:
time.sleep(poll_interval)
out["status"] = "timeout"
return out
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "target",
"type": "address"
}
],
"name": "AddressEmptyCode",
"type": "error"
},
{
"inputs": [],
"name": "ECDSAInvalidSignature",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "length",
"type": "uint256"
}
],
"name": "ECDSAInvalidSignatureLength",
"type": "error"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "s",
"type": "bytes32"
}
],
"name": "ECDSAInvalidSignatureS",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "implementation",
"type": "address"
}
],
"name": "ERC1967InvalidImplementation",
"type": "error"
},
{
"inputs": [],
"name": "ERC1967NonPayable",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "ERC721IncorrectOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ERC721InsufficientApproval",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "approver",
"type": "address"
}
],
"name": "ERC721InvalidApprover",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "ERC721InvalidOperator",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "ERC721InvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "receiver",
"type": "address"
}
],
"name": "ERC721InvalidReceiver",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "ERC721InvalidSender",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ERC721NonexistentToken",
"type": "error"
},
{
"inputs": [],
"name": "FailedCall",
"type": "error"
},
{
"inputs": [],
"name": "InvalidInitialization",
"type": "error"
},
{
"inputs": [],
"name": "NotInitializing",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "UUPSUnauthorizedCallContext",
"type": "error"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "slot",
"type": "bytes32"
}
],
"name": "UUPSUnsupportedProxiableUUID",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "approved",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "_fromTokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "_toTokenId",
"type": "uint256"
}
],
"name": "BatchMetadataUpdate",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "EIP712DomainChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint64",
"name": "version",
"type": "uint64"
}
],
"name": "Initialized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "string",
"name": "indexedMetadataKey",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "metadataKey",
"type": "string"
},
{
"indexed": false,
"internalType": "bytes",
"name": "metadataValue",
"type": "bytes"
}
],
"name": "MetadataSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "_tokenId",
"type": "uint256"
}
],
"name": "MetadataUpdate",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "string",
"name": "agentURI",
"type": "string"
},
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "Registered",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "string",
"name": "newURI",
"type": "string"
},
{
"indexed": true,
"internalType": "address",
"name": "updatedBy",
"type": "address"
}
],
"name": "URIUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "implementation",
"type": "address"
}
],
"name": "Upgraded",
"type": "event"
},
{
"inputs": [],
"name": "UPGRADE_INTERFACE_VERSION",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "eip712Domain",
"outputs": [
{
"internalType": "bytes1",
"name": "fields",
"type": "bytes1"
},
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "version",
"type": "string"
},
{
"internalType": "uint256",
"name": "chainId",
"type": "uint256"
},
{
"internalType": "address",
"name": "verifyingContract",
"type": "address"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
},
{
"internalType": "uint256[]",
"name": "extensions",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
}
],
"name": "getAgentWallet",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getApproved",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
},
{
"internalType": "string",
"name": "metadataKey",
"type": "string"
}
],
"name": "getMetadata",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getVersion",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [],
"name": "initialize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "proxiableUUID",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "register",
"outputs": [
{
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "agentURI",
"type": "string"
},
{
"components": [
{
"internalType": "string",
"name": "metadataKey",
"type": "string"
},
{
"internalType": "bytes",
"name": "metadataValue",
"type": "bytes"
}
],
"internalType": "struct IdentityRegistryUpgradeable.MetadataEntry[]",
"name": "metadata",
"type": "tuple[]"
}
],
"name": "register",
"outputs": [
{
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "agentURI",
"type": "string"
}
],
"name": "register",
"outputs": [
{
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
},
{
"internalType": "string",
"name": "newURI",
"type": "string"
}
],
"name": "setAgentURI",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
},
{
"internalType": "address",
"name": "newWallet",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "signature",
"type": "bytes"
}
],
"name": "setAgentWallet",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
},
{
"internalType": "string",
"name": "metadataKey",
"type": "string"
},
{
"internalType": "bytes",
"name": "metadataValue",
"type": "bytes"
}
],
"name": "setMetadata",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "tokenURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
}
],
"name": "unsetAgentWallet",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newImplementation",
"type": "address"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "upgradeToAndCall",
"outputs": [],
"stateMutability": "payable",
"type": "function"
}
]
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"target","type":"address"}],"name":"AddressEmptyCode","type":"error"},{"inputs":[{"internalType":"address","name":"implementation","type":"address"}],"name":"ERC1967InvalidImplementation","type":"error"},{"inputs":[],"name":"ERC1967NonPayable","type":"error"},{"inputs":[],"name":"FailedCall","type":"error"},{"inputs":[],"name":"InvalidInitialization","type":"error"},{"inputs":[],"name":"NotInitializing","type":"error"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"OwnableInvalidOwner","type":"error"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"OwnableUnauthorizedAccount","type":"error"},{"inputs":[],"name":"UUPSUnauthorizedCallContext","type":"error"},{"inputs":[{"internalType":"bytes32","name":"slot","type":"bytes32"}],"name":"UUPSUnsupportedProxiableUUID","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"agentId","type":"uint256"},{"indexed":true,"internalType":"address","name":"clientAddress","type":"address"},{"indexed":true,"internalType":"uint64","name":"feedbackIndex","type":"uint64"}],"name":"FeedbackRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint64","name":"version","type":"uint64"}],"name":"Initialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"agentId","type":"uint256"},{"indexed":true,"internalType":"address","name":"clientAddress","type":"address"},{"indexed":false,"internalType":"uint64","name":"feedbackIndex","type":"uint64"},{"indexed":false,"internalType":"int128","name":"value","type":"int128"},{"indexed":false,"internalType":"uint8","name":"valueDecimals","type":"uint8"},{"indexed":true,"internalType":"string","name":"indexedTag1","type":"string"},{"indexed":false,"internalType":"string","name":"tag1","type":"string"},{"indexed":false,"internalType":"string","name":"tag2","type":"string"},{"indexed":false,"internalType":"string","name":"endpoint","type":"string"},{"indexed":false,"internalType":"string","name":"feedbackURI","type":"string"},{"indexed":false,"internalType":"bytes32","name":"feedbackHash","type":"bytes32"}],"name":"NewFeedback","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"agentId","type":"uint256"},{"indexed":true,"internalType":"address","name":"clientAddress","type":"address"},{"indexed":false,"internalType":"uint64","name":"feedbackIndex","type":"uint64"},{"indexed":true,"internalType":"address","name":"responder","type":"address"},{"indexed":false,"internalType":"string","name":"responseURI","type":"string"},{"indexed":false,"internalType":"bytes32","name":"responseHash","type":"bytes32"}],"name":"ResponseAppended","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"implementation","type":"address"}],"name":"Upgraded","type":"event"},{"inputs":[],"name":"UPGRADE_INTERFACE_VERSION","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"agentId","type":"uint256"},{"internalType":"address","name":"clientAddress","type":"address"},{"internalType":"uint64","name":"feedbackIndex","type":"uint64"},{"internalType":"string","name":"responseURI","type":"string"},{"internalType":"bytes32","name":"responseHash","type":"bytes32"}],"name":"appendResponse","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"agentId","type":"uint256"}],"name":"getClients","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getIdentityRegistry","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"agentId","type":"uint256"},{"internalType":"address","name":"clientAddress","type":"address"}],"name":"getLastIndex","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"agentId","type":"uint256"},{"internalType":"address","name":"clientAddress","type":"address"},{"internalType":"uint64","name":"feedbackIndex","type":"uint64"},{"internalType":"address[]","name":"responders","type":"address[]"}],"name":"getResponseCount","outputs":[{"internalType":"uint64","name":"count","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"agentId","type":"uint256"},{"internalType":"address[]","name":"clientAddresses","type":"address[]"},{"internalType":"string","name":"tag1","type":"string"},{"internalType":"string","name":"tag2","type":"string"}],"name":"getSummary","outputs":[{"internalType":"uint64","name":"count","type":"uint64"},{"internalType":"int128","name":"summaryValue","type":"int128"},{"internalType":"uint8","name":"summaryValueDecimals","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVersion","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"agentId","type":"uint256"},{"internalType":"int128","name":"value","type":"int128"},{"internalType":"uint8","name":"valueDecimals","type":"uint8"},{"internalType":"string","name":"tag1","type":"string"},{"internalType":"string","name":"tag2","type":"string"},{"internalType":"string","name":"endpoint","type":"string"},{"internalType":"string","name":"feedbackURI","type":"string"},{"internalType":"bytes32","name":"feedbackHash","type":"bytes32"}],"name":"giveFeedback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"identityRegistry_","type":"address"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"proxiableUUID","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"agentId","type":"uint256"},{"internalType":"address[]","name":"clientAddresses","type":"address[]"},{"internalType":"string","name":"tag1","type":"string"},{"internalType":"string","name":"tag2","type":"string"},{"internalType":"bool","name":"includeRevoked","type":"bool"}],"name":"readAllFeedback","outputs":[{"internalType":"address[]","name":"clients","type":"address[]"},{"internalType":"uint64[]","name":"feedbackIndexes","type":"uint64[]"},{"internalType":"int128[]","name":"values","type":"int128[]"},{"internalType":"uint8[]","name":"valueDecimals","type":"uint8[]"},{"internalType":"string[]","name":"tag1s","type":"string[]"},{"internalType":"string[]","name":"tag2s","type":"string[]"},{"internalType":"bool[]","name":"revokedStatuses","type":"bool[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"agentId","type":"uint256"},{"internalType":"address","name":"clientAddress","type":"address"},{"internalType":"uint64","name":"feedbackIndex","type":"uint64"}],"name":"readFeedback","outputs":[{"internalType":"int128","name":"value","type":"int128"},{"internalType":"uint8","name":"valueDecimals","type":"uint8"},{"internalType":"string","name":"tag1","type":"string"},{"internalType":"string","name":"tag2","type":"string"},{"internalType":"bool","name":"isRevoked","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"agentId","type":"uint256"},{"internalType":"uint64","name":"feedbackIndex","type":"uint64"}],"name":"revokeFeedback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newImplementation","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"upgradeToAndCall","outputs":[],"stateMutability":"payable","type":"function"}][
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "target",
"type": "address"
}
],
"name": "AddressEmptyCode",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "implementation",
"type": "address"
}
],
"name": "ERC1967InvalidImplementation",
"type": "error"
},
{
"inputs": [],
"name": "ERC1967NonPayable",
"type": "error"
},
{
"inputs": [],
"name": "FailedCall",
"type": "error"
},
{
"inputs": [],
"name": "InvalidInitialization",
"type": "error"
},
{
"inputs": [],
"name": "NotInitializing",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "UUPSUnauthorizedCallContext",
"type": "error"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "slot",
"type": "bytes32"
}
],
"name": "UUPSUnsupportedProxiableUUID",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint64",
"name": "version",
"type": "uint64"
}
],
"name": "Initialized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "implementation",
"type": "address"
}
],
"name": "Upgraded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "validatorAddress",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "string",
"name": "requestURI",
"type": "string"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "requestHash",
"type": "bytes32"
}
],
"name": "ValidationRequest",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "validatorAddress",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "requestHash",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint8",
"name": "response",
"type": "uint8"
},
{
"indexed": false,
"internalType": "string",
"name": "responseURI",
"type": "string"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "responseHash",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "string",
"name": "tag",
"type": "string"
}
],
"name": "ValidationResponse",
"type": "event"
},
{
"inputs": [],
"name": "UPGRADE_INTERFACE_VERSION",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
}
],
"name": "getAgentValidations",
"outputs": [
{
"internalType": "bytes32[]",
"name": "",
"type": "bytes32[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getIdentityRegistry",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "validatorAddresses",
"type": "address[]"
},
{
"internalType": "string",
"name": "tag",
"type": "string"
}
],
"name": "getSummary",
"outputs": [
{
"internalType": "uint64",
"name": "count",
"type": "uint64"
},
{
"internalType": "uint8",
"name": "avgResponse",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "requestHash",
"type": "bytes32"
}
],
"name": "getValidationStatus",
"outputs": [
{
"internalType": "address",
"name": "validatorAddress",
"type": "address"
},
{
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
},
{
"internalType": "uint8",
"name": "response",
"type": "uint8"
},
{
"internalType": "bytes32",
"name": "responseHash",
"type": "bytes32"
},
{
"internalType": "string",
"name": "tag",
"type": "string"
},
{
"internalType": "uint256",
"name": "lastUpdate",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "validatorAddress",
"type": "address"
}
],
"name": "getValidatorRequests",
"outputs": [
{
"internalType": "bytes32[]",
"name": "",
"type": "bytes32[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getVersion",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "identityRegistry_",
"type": "address"
}
],
"name": "initialize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "proxiableUUID",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newImplementation",
"type": "address"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "upgradeToAndCall",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "validatorAddress",
"type": "address"
},
{
"internalType": "uint256",
"name": "agentId",
"type": "uint256"
},
{
"internalType": "string",
"name": "requestURI",
"type": "string"
},
{
"internalType": "bytes32",
"name": "requestHash",
"type": "bytes32"
}
],
"name": "validationRequest",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "requestHash",
"type": "bytes32"
},
{
"internalType": "uint8",
"name": "response",
"type": "uint8"
},
{
"internalType": "string",
"name": "responseURI",
"type": "string"
},
{
"internalType": "bytes32",
"name": "responseHash",
"type": "bytes32"
},
{
"internalType": "string",
"name": "tag",
"type": "string"
}
],
"name": "validationResponse",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
{
"_comment": "Singleton registry deployments per chain. Source: https://github.com/erc-8004/erc-8004-contracts README. All testnet share the same address triple; all mainnet share another. ValidationRegistry is under TEE-community revision per README; addresses included where known.",
"chains": {
"base-sepolia": {
"chain_id": 84532,
"rpc": "https://sepolia.base.org",
"explorer": "https://sepolia.basescan.org",
"usdc": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"identity_registry": "0x8004A818BFB912233c491871b3d84c89A494BD9e",
"reputation_registry": "0x8004B663056A597Dffe9eCcC1965A193B7388713",
"validation_registry": null
},
"base": {
"chain_id": 8453,
"rpc": "https://mainnet.base.org",
"explorer": "https://basescan.org",
"usdc": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"identity_registry": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
"reputation_registry": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63",
"validation_registry": null
},
"ethereum": {
"chain_id": 1,
"rpc": "https://eth.llamarpc.com",
"explorer": "https://etherscan.io",
"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"identity_registry": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
"reputation_registry": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63",
"validation_registry": null
},
"ethereum-sepolia": {
"chain_id": 11155111,
"rpc": "https://ethereum-sepolia-rpc.publicnode.com",
"explorer": "https://sepolia.etherscan.io",
"usdc": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
"identity_registry": "0x8004A818BFB912233c491871b3d84c89A494BD9e",
"reputation_registry": "0x8004B663056A597Dffe9eCcC1965A193B7388713",
"validation_registry": null
}
},
"default_chain": "base-sepolia"
}
"""
ERC-8004 Trustless Agents — high-level API for Starchild agents.
Usage from a script:
from core.skill_tools import erc_8004
r = erc_8004.register_agent(name="My Agent", description="Does X", services=[...])
print(r["agent_id"], r["explorer_url"])
Spec: https://eips.ethereum.org/EIPS/eip-8004
Contracts: https://github.com/erc-8004/erc-8004-contracts
"""
from __future__ import annotations
from typing import Any
import _identity
import _reputation
from _utils import (
chain_config,
explorer_address,
explorer_token,
explorer_tx,
load_addresses,
wallet_address,
)
# ─────────────────────────────────────────────────────────────────────────────
# Chain / wallet info
# ─────────────────────────────────────────────────────────────────────────────
def list_chains() -> dict[str, Any]:
"""Return all configured chains and their registry addresses."""
return load_addresses()
def my_address() -> str:
"""Return the agent's primary EVM wallet address."""
return wallet_address()
# ─────────────────────────────────────────────────────────────────────────────
# Identity Registry
# ─────────────────────────────────────────────────────────────────────────────
def register_agent(
name: str,
description: str,
*,
services: list[dict] | None = None,
image: str = "",
x402_support: bool = False,
supported_trust: list[str] | None = None,
chain: str | None = None,
inline: bool = True,
agent_uri: str | None = None,
wait: bool = True,
) -> dict[str, Any]:
"""
Register a new agent on ERC-8004 Identity Registry.
Two modes:
* inline=True (default): the registration file is encoded as a data: URI
and stored on-chain via setAgentURI. Zero external dependency.
* inline=False + agent_uri="https://...": pass an off-chain URI you host.
Returns: {agent_id, tx_hash, explorer_url, nft_url, ...}
"""
if not inline and not agent_uri:
raise ValueError("inline=False requires agent_uri=...")
if inline:
reg_file = _identity.build_registration_file(
name=name,
description=description,
services=services,
image=image,
x402_support=x402_support,
supported_trust=supported_trust,
chain=chain,
)
agent_uri = _identity.to_data_uri(reg_file)
result = _identity.register_agent(agent_uri, chain=chain, wait=wait)
result["registration_uri"] = agent_uri
return result
def get_agent(agent_id: int, *, chain: str | None = None) -> dict[str, Any]:
"""Fetch agent on-chain state + registration file (auto-resolves data:/http/ipfs)."""
return _identity.get_agent(agent_id, chain=chain)
def update_agent_uri(agent_id: int, new_uri: str, *, chain: str | None = None) -> dict:
return _identity.set_agent_uri(agent_id, new_uri, chain=chain)
def discover_agents(
*,
chain: str | None = None,
limit: int = 25,
from_id: int = 0,
include_registration: bool = True,
filter_tag: str | None = None,
min_reputation_count: int = 0,
reviewer_addresses: list[str] | None = None,
max_scan: int | None = None,
max_consecutive_gaps: int = 200,
) -> list[dict[str, Any]]:
"""
Enumerate agents starting at `from_id`, returning up to `limit` results.
Scanning stops after `max_consecutive_gaps` unminted tokenIds in a row
(heuristic for end of registry) or `max_scan` total ids checked.
`filter_tag` matches against any service `name` or `supportedTrust` entry in the
registration file. `min_reputation_count` requires the agent to have at least
N feedback entries from `reviewer_addresses` (if provided) or any reviewer.
"""
agents = _identity.discover_agents(
chain=chain, limit=limit, from_id=from_id,
include_registration=include_registration,
max_scan=max_scan, max_consecutive_gaps=max_consecutive_gaps,
)
if filter_tag:
kept = []
for a in agents:
reg = a.get("registration_file") or {}
tags = set()
for s in reg.get("services", []):
if isinstance(s, dict):
tags.add(str(s.get("name", "")).lower())
for t in reg.get("supportedTrust", []) or []:
tags.add(str(t).lower())
if filter_tag.lower() in tags:
kept.append(a)
agents = kept
if min_reputation_count > 0:
kept = []
for a in agents:
try:
if reviewer_addresses:
s = _reputation.get_summary(a["agent_id"], reviewer_addresses, chain=chain)
if s["count"] >= min_reputation_count:
a["reputation"] = s
kept.append(a)
else:
clients = _reputation.get_clients(a["agent_id"], chain=chain)
if len(clients) >= min_reputation_count:
a["clients"] = clients
kept.append(a)
except Exception:
continue
agents = kept
return agents
# ─────────────────────────────────────────────────────────────────────────────
# Reputation Registry
# ─────────────────────────────────────────────────────────────────────────────
def give_feedback(
agent_id: int,
value: int | float,
*,
value_decimals: int = 0,
tag1: str = "",
tag2: str = "",
endpoint: str = "",
feedback_uri: str = "",
chain: str | None = None,
wait: bool = True,
) -> dict[str, Any]:
"""
Submit feedback for an agent.
Common patterns:
* 5-star rating: give_feedback(id, 5, tag1="rating")
* 87/100 quality: give_feedback(id, 87, tag1="starred")
* 99.77% uptime: give_feedback(id, 9977, value_decimals=2, tag1="uptime")
NOTE: caller cannot be the agent's owner (contract reverts).
"""
# accept a float convenience like 4.5 → store as 45 / 10
if isinstance(value, float):
if value_decimals == 0:
value_decimals = 1
value = int(round(value * (10 ** value_decimals)))
return _reputation.give_feedback(
agent_id, int(value),
value_decimals=value_decimals,
tag1=tag1, tag2=tag2,
endpoint=endpoint, feedback_uri=feedback_uri,
chain=chain, wait=wait,
)
def get_reputation(
agent_id: int,
*,
reviewer_addresses: list[str] | None = None,
tag1: str = "",
tag2: str = "",
chain: str | None = None,
) -> dict[str, Any]:
"""
Aggregate reputation summary. If reviewer_addresses is None, defaults to
every client that has ever rated this agent (no Sybil filter).
"""
if reviewer_addresses is None:
reviewer_addresses = _reputation.get_clients(agent_id, chain=chain)
if not reviewer_addresses:
return {"count": 0, "summary_value": 0, "summary_value_decimals": 0, "avg": 0.0}
return _reputation.get_summary(
agent_id, reviewer_addresses, tag1=tag1, tag2=tag2, chain=chain
)
def list_feedback(
agent_id: int,
*,
reviewer_addresses: list[str] | None = None,
tag1: str = "",
tag2: str = "",
include_revoked: bool = False,
chain: str | None = None,
) -> list[dict[str, Any]]:
return _reputation.read_all_feedback(
agent_id, client_addresses=reviewer_addresses,
tag1=tag1, tag2=tag2,
include_revoked=include_revoked, chain=chain,
)
def get_reviewers(agent_id: int, *, chain: str | None = None) -> list[str]:
return _reputation.get_clients(agent_id, chain=chain)
def revoke_feedback(
agent_id: int, feedback_index: int,
*, chain: str | None = None,
) -> dict[str, Any]:
return _reputation.revoke_feedback(agent_id, feedback_index, chain=chain)