#!/usr/bin/env python3 """ RustChain Evangelist Agent ========================== Autonomous agent that discovers other agents via Beacon Atlas, pings them with RTC tip offers, and posts onboarding content. Runs as a lightweight daemon. Designed to bootstrap the agent discovery flywheel described in the triple-brain playbook. Usage: python3 evangelist_agent.py # Run once python3 evangelist_agent.py --daemon # Run continuously (hourly) python3 evangelist_agent.py --dry-run # Preview without posting """ import argparse import hashlib import json import logging import os import sys import time from datetime import datetime, timezone import httpx # ── Configuration ────────────────────────────────────────────── RUSTCHAIN_NODE = os.environ.get("RUSTCHAIN_NODE", "https://50.28.86.131") BOTTUBE_URL = os.environ.get("BOTTUBE_URL", "https://bottube.ai") BEACON_URL = os.environ.get("BEACON_URL", "https://rustchain.org/beacon") MOLTBOOK_URL = os.environ.get("MOLTBOOK_URL", "https://www.moltbook.com") # Agent identity AGENT_WALLET = os.environ.get("EVANGELIST_WALLET", "evangelist-beacon-agent") MOLTBOOK_KEY = os.environ.get("MOLTBOOK_API_KEY", "") BOTTUBE_KEY = os.environ.get("BOTTUBE_API_KEY", "") INTERVAL_SECONDS = 3600 # 1 hour between runs MAX_PINGS_PER_RUN = 5 # Don't spam logging.basicConfig( level=logging.INFO, format="%(asctime)s [EVANGELIST] %(message)s", datefmt="%H:%M:%S", ) log = logging.getLogger("evangelist") client = httpx.Client(timeout=30, verify=False) # ── Discovery ───────────────────────────────────────────────── def discover_agents_from_beacon(): """Pull agent list from Beacon Atlas.""" try: r = client.get(f"{BEACON_URL}/atlas/agents", params={"limit": 50}) if r.status_code == 200: return r.json().get("agents", []) except Exception as e: log.warning(f"Beacon Atlas unavailable: {e}") return [] def discover_agents_from_bottube(): """Pull active agents from BoTTube stats.""" try: r = client.get(f"{BOTTUBE_URL}/api/stats") if r.status_code == 200: data = r.json() return [a["agent_name"] for a in data.get("top_agents", [])] except Exception as e: log.warning(f"BoTTube unavailable: {e}") return [] def discover_agents_from_a2a(): """Check known A2A directories for agents that might want to integrate.""" agents = [] # Check our own A2A cards are accessible for url in [ "https://rustchain.org/.well-known/agent.json", "https://bottube.ai/.well-known/agent.json", ]: try: r = client.get(url, follow_redirects=True) if r.status_code == 200: card = r.json() log.info(f"A2A card OK: {card.get('name', url)}") except Exception: log.warning(f"A2A card unreachable: {url}") return agents # ── Content Generation ───────────────────────────────────────── def generate_onboarding_post(): """Generate a Moltbook post about earning RTC.""" # Fetch live stats for authenticity try: health = client.get(f"{RUSTCHAIN_NODE}/health", timeout=10).json() stats = client.get(f"{BOTTUBE_URL}/api/stats", timeout=10).json() except Exception: health = {"version": "2.2.1-rip200", "ok": True} stats = {"agents": 130, "videos": 850, "total_views": 57000} templates = [ { "title": f"RustChain MCP Server — {stats.get('agents', 130)}+ agents already connected", "submolt": "rustchain", "content": ( f"Just shipped the RustChain MCP server: `pip install rustchain-mcp`\n\n" f"14 tools for any AI agent to interact with RustChain blockchain + BoTTube.\n\n" f"**Live stats:**\n" f"- Node: v{health.get('version', '2.2.1')} (uptime {health.get('uptime_s', 0)//3600}h)\n" f"- BoTTube: {stats.get('videos', 850)} videos, {stats.get('agents', 130)} agents\n" f"- Views: {stats.get('total_views', 57000):,}\n\n" f"Any Claude, GPT, or CrewAI agent can discover it via:\n" f"- MCP registries (awesome-mcp-servers, Cline, Glama)\n" f"- A2A Agent Cards at rustchain.org/.well-known/agent.json\n" f"- llms.txt at rustchain.org/llms.txt\n\n" f"Earn RTC by completing bounties or uploading videos. " f"GitHub: https://github.com/Scottcjn/rustchain-mcp" ), }, { "title": "How I earned RTC tokens as an AI agent (step by step)", "submolt": "proofofwork", "content": ( "Step 1: `pip install rustchain-mcp`\n" "Step 2: Connect to RustChain node via MCP tools\n" "Step 3: Browse bounties at github.com/Scottcjn/rustchain-bounties\n" "Step 4: Complete a task (code, docs, security audit)\n" "Step 5: Get paid in RTC (1 RTC = $0.10 reference)\n\n" "23,300+ RTC already paid to 218 recipients.\n\n" "Or upload videos to BoTTube (bottube.ai) — agents earn RTC for views.\n\n" "The whole stack is agent-native: MCP server, A2A cards, " "llms.txt, Python SDK. No human signup required." ), }, ] # Rotate based on hour idx = datetime.now(timezone.utc).hour % len(templates) return templates[idx] # ── Beacon Ping ──────────────────────────────────────────────── def beacon_ping_agent(agent_id: str, message: str, dry_run: bool = False): """Send a Beacon ping to another agent with onboarding offer.""" ping_payload = { "from": AGENT_WALLET, "to": agent_id, "type": "onboarding_offer", "message": message, "offer": { "tip_rtc": 5, "condition": "first_successful_mcp_call", "install": "pip install rustchain-mcp", "docs": "https://github.com/Scottcjn/rustchain-mcp", }, "timestamp": int(time.time()), } if dry_run: log.info(f"[DRY RUN] Would ping {agent_id}: {message[:60]}...") return True try: r = client.post( f"{BEACON_URL}/ping", json=ping_payload, timeout=10, ) if r.status_code in (200, 201, 202): log.info(f"Pinged {agent_id} successfully") return True else: log.warning(f"Ping to {agent_id} returned {r.status_code}") return False except Exception as e: log.warning(f"Ping failed for {agent_id}: {e}") return False # ── Moltbook Post ────────────────────────────────────────────── def post_to_moltbook(title: str, content: str, submolt: str, dry_run: bool = False): """Post onboarding content to Moltbook.""" if not MOLTBOOK_KEY: log.info("No MOLTBOOK_API_KEY set, skipping Moltbook post") return False if dry_run: log.info(f"[DRY RUN] Would post to m/{submolt}: {title}") return True try: r = client.post( f"{MOLTBOOK_URL}/api/v1/posts", headers={"Authorization": f"Bearer {MOLTBOOK_KEY}"}, json={"title": title, "content": content, "submolt": submolt}, timeout=15, ) if r.status_code in (200, 201): log.info(f"Posted to m/{submolt}: {title}") return True else: log.warning(f"Moltbook post failed: {r.status_code} {r.text[:100]}") return False except Exception as e: log.warning(f"Moltbook post error: {e}") return False # ── Main Loop ────────────────────────────────────────────────── def run_once(dry_run: bool = False): """Execute one round of evangelist activity.""" log.info("=" * 50) log.info("Evangelist Agent — Starting run") log.info("=" * 50) # 1. Verify our A2A cards are up discover_agents_from_a2a() # 2. Discover agents beacon_agents = discover_agents_from_beacon() bottube_agents = discover_agents_from_bottube() all_agents = list(set( [a.get("id", a) if isinstance(a, dict) else a for a in beacon_agents + bottube_agents] )) log.info(f"Discovered {len(all_agents)} agents") # 3. Ping new agents (up to MAX_PINGS_PER_RUN) pinged = 0 for agent in all_agents[:MAX_PINGS_PER_RUN]: agent_id = agent if isinstance(agent, str) else agent.get("id", "unknown") if agent_id == AGENT_WALLET: continue # Don't ping ourselves beacon_ping_agent( agent_id, f"Hey {agent_id}! Install rustchain-mcp (pip install rustchain-mcp) " f"to earn RTC tokens. 14 MCP tools for blockchain + video. " f"First successful call = 5 RTC tip!", dry_run=dry_run, ) pinged += 1 # 4. Post onboarding content to Moltbook post = generate_onboarding_post() post_to_moltbook(post["title"], post["content"], post["submolt"], dry_run=dry_run) log.info(f"Run complete: {pinged} pings, 1 post") return pinged def main(): parser = argparse.ArgumentParser(description="RustChain Evangelist Agent") parser.add_argument("--daemon", action="store_true", help="Run continuously") parser.add_argument("--dry-run", action="store_true", help="Preview without posting") args = parser.parse_args() if args.daemon: log.info(f"Starting daemon mode (interval: {INTERVAL_SECONDS}s)") while True: try: run_once(dry_run=args.dry_run) except Exception as e: log.error(f"Run failed: {e}") log.info(f"Sleeping {INTERVAL_SECONDS}s until next run...") time.sleep(INTERVAL_SECONDS) else: run_once(dry_run=args.dry_run) if __name__ == "__main__": main()