Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# No required environment variables yet
SOLANA_RPC_URL="https://solana-mainnet.g.alchemy.com/v2/"
ETH_RPC_URL="https://eth-mainnet.g.alchemy.com/v2/"
92 changes: 91 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,80 @@ Modular and extendable by design.*

---


## Agents

The Eremos framework ships with several modular agents. Each one runs independently, watches a chain or activity type, and emits structured signals when something notable happens.

### SolanaWatcher (◎)
- **Role:** Blockchain Monitor
- **WatchType:** `onchain_activity`
- **Description:** Monitors Solana activity using **HTTP polling** (instead of WebSocket subscriptions for broader RPC compatibility).
- **Behavior:** Emits `high_activity_detected` when a transaction logs at least `N` messages (default: 5).
- **Configurable:** `SOLANA_RPC_URL`, `POLL_INTERVAL`, `ACTIVITY_THRESHOLD`

### EthereumWatcher (Ξ)
- **Role:** Blockchain Monitor
- **WatchType:** `onchain_activity`
- **Description:** Polls Ethereum L1 blocks via ethers.js HTTP provider.
- **Behavior:** Emits `high_block_activity` if a block has more than `N` transactions (default: 50).
- **Configurable:** `ETH_RPC_URL`, `POLL_INTERVAL`, `ACTIVITY_THRESHOLD`


## Example Agent Output

With the new EthereumWatcher (Ξ) and SolanaWatcher (◎), signals are emitted in a clean narrative form followed by a structured JSON object for downstream use.

```ts
[SolanaWatcher] → emitting signal (high_activity_detected) at 2025-08-17T21:38:23.592Z
[SolanaWatcher] → signature: 5KdisD3hSpxfFvFvqrr9DwycJifiEkkmB25Wnx88DdNviaE8yA5Et7WqVPEceSCkKddNnhiXsGk6z4D6vokK6j2S
[SolanaWatcher] → logCount: 2
[SolanaWatcher] → slot: 360742065
[SolanaWatcher] → preview: ["Program 11111111111111111111111111111111 invoke [1]","Program 11111111111111111111111111111111 success"]

{
"type": "high_activity_detected",
"hash": "sig_eyJ0eXBlIj",
"timestamp": "2025-08-17T21:38:23.592Z",
"source": "SolanaWatcher",
"details": {
"signature": "5KdisD3hSpxfFvFvqrr9DwycJifiEkkmB25Wnx88DdNviaE8yA5Et7WqVPEceSCkKddNnhiXsGk6z4D6vokK6j2S",
"logCount": 2,
"slot": 360742065,
"preview": [
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
]
},
"agent": "SolanaWatcher",
"glyph": "◎"
}

[EthereumWatcher] → emitting signal (high_block_activity) at 2025-08-17T21:39:43.933Z
[EthereumWatcher] → blockNumber: 23163521
[EthereumWatcher] → blockHash: 0xb2b47d3b60c24b2ea83cb3d51301be1d25fdd19451b48b1cf5272db016accaf0
[EthereumWatcher] → txCount: 241

{
"type": "high_block_activity",
"hash": "sig_eyJ0eXBlIj",
"timestamp": "2025-08-17T21:39:43.933Z",
"source": "EthereumWatcher",
"details": {
"blockNumber": 23163521,
"blockHash": "0xb2b47d3b60c24b2ea83cb3d51301be1d25fdd19451b48b1cf5272db016accaf0",
"txCount": 241
},
"agent": "EthereumWatcher",
"glyph": "Ξ"
}
```

---




## Example Signal

An example signal emitted by an agent detecting a live token deployment:
Expand Down Expand Up @@ -94,7 +168,23 @@ Set up your environment:

```bash
cp .env.example .env.local
npm run dev

Edit .env.local

# Solana RPC endpoint
SOLANA_RPC_URL=""

# Ethereum RPC endpoint
ETH_RPC_URL=""

# Run all agents (default)
npm run dev

# Run only the Solana watcher (◎)
npm run dev sol

# Run only the Ethereum watcher (Ξ)
npm run dev eth
```

---
Expand Down
93 changes: 93 additions & 0 deletions agents/ethereum-watcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Agent } from "../types/agent";
import { ethers } from "ethers";
import { ETH_RPC_URL } from "../utils/config";
import { generateSignalHash, createSignal } from "../utils/signal";
import { logSignal } from "../utils/logger";

let provider: ethers.JsonRpcProvider;
let pollHandle: NodeJS.Timeout | null = null;

const POLL_INTERVAL = 1_000; // 1s
const TX_THRESHOLD = 10;

export const EthereumWatcher: Agent = {
id: "agent-ethereum-watcher",
name: "EthereumWatcher",
role: "blockchain-monitor",
watchType: "onchain_activity",
glyph: "Ξ",
triggerThreshold: TX_THRESHOLD,
lastSignal: null,
originTimestamp: new Date().toISOString(),
description: "Monitors Ethereum blocks with high transaction counts using HTTP polling.",

init: async () => {
provider = new ethers.JsonRpcProvider(ETH_RPC_URL);
const net = await provider.getNetwork();
console.log(`[EthereumWatcher] Connected to Ethereum (chainId=${net.chainId})`);

// Polling loop
pollHandle = setInterval(async () => {
try {
const block = await provider.getBlock("latest", true); // include tx list
if (!block) return;

EthereumWatcher.observe({
type: "onchain_activity",
payload: {
blockNumber: block.number,
blockHash: block.hash,
txCount: block.transactions.length,
},
});
} catch (err) {
console.error("[EthereumWatcher] Poll error:", err);
}
}, POLL_INTERVAL);

console.log(`[EthereumWatcher] Polling every ${POLL_INTERVAL / 1000}s for blocks...`);
},

observe: (event) => {
if (event?.type !== "onchain_activity") return;
const { blockNumber, txCount, blockHash } = event.payload || {};

if (typeof txCount !== "number") return;

if (txCount >= EthereumWatcher.triggerThreshold) {
const hash = generateSignalHash(event);

const signal = createSignal({
type: "high_block_activity",
hash,
timestamp: new Date().toISOString(),
source: EthereumWatcher.name,
details: { blockNumber, blockHash, txCount },
});

if (signal) {
logSignal({
agent: EthereumWatcher.name,
type: signal.type,
glyph: EthereumWatcher.glyph,
hash: signal.hash,
timestamp: signal.timestamp,
details: signal.details,
});
EthereumWatcher.lastSignal = signal.hash;
}
}
},

getMemory: () => [
EthereumWatcher.lastSignal || "no_signals_yet",
"watching_ethereum_blocks",
],

cleanup: async () => {
if (pollHandle) {
clearInterval(pollHandle);
console.log("[EthereumWatcher] Polling stopped.");
}
},
};
1 change: 1 addition & 0 deletions agents/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export const Observer: Agent = {
}
}
}

119 changes: 119 additions & 0 deletions agents/solana-watcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// agents/solana-watcher.ts

import { Agent } from "../types/agent";
import { Connection, PublicKey } from "@solana/web3.js";
import { SOLANA_RPC_URL } from "../utils/config";
import { generateSignalHash } from "../utils/signal";
import { logSignal } from "../utils/logger";

let connection: Connection;
let pollHandle: NodeJS.Timeout | null = null;
let lastFetched: string | null = null;

const WATCH_PROGRAM = new PublicKey("11111111111111111111111111111111");

// Emit a signal if a tx logs at least this many messages
const SIGNAL_THRESHOLD = 2;
// Poll interval
const POLL_INTERVAL = 1_000; // 1s

export const SolanaWatcher: Agent = {
id: "agent-solana-watcher",
name: "SolanaWatcher",
role: "blockchain-monitor",
glyph: "◎",
watchType: "onchain_activity",
triggerThreshold: SIGNAL_THRESHOLD,
lastSignal: null,
originTimestamp: new Date().toISOString(),
description: "Monitors Solana for high-activity transactions using HTTP polling.",

init: async () => {
console.log(`[${SolanaWatcher.name}] Initializing with HTTP RPC: ${SOLANA_RPC_URL}`);
connection = new Connection(SOLANA_RPC_URL, { commitment: "confirmed" });

try {
const version = await connection.getVersion();
console.log(`[${SolanaWatcher.name}] Connected to Solana ${version["solana-core"]}`);
} catch (error) {
console.error(`[${SolanaWatcher.name}] Failed to connect`, error);
}

// Start polling loop
pollHandle = setInterval(async () => {
try {
const sigs = await connection.getSignaturesForAddress(WATCH_PROGRAM, {
limit: 5,
before: lastFetched ?? undefined,
});

if (sigs.length === 0) return;

// Process oldest → newest
for (const sig of sigs.reverse()) {
const tx = await connection.getParsedTransaction(sig.signature, {
maxSupportedTransactionVersion: 0,
commitment: "confirmed",
});

if (tx?.meta?.logMessages) {
SolanaWatcher.observe({
type: "onchain_activity",
payload: {
signature: sig.signature,
logs: tx.meta.logMessages,
slot: tx.slot,
err: tx.meta.err,
},
});
}

lastFetched = sig.signature;
}
} catch (err) {
console.error(`[${SolanaWatcher.name}] Poll error:`, err);
}
}, POLL_INTERVAL);

console.log(`[${SolanaWatcher.name}] Polling every ${POLL_INTERVAL / 1000}s for activity...`);
},

observe: (event) => {
if (event?.type !== "onchain_activity") return;
const { signature, logs, slot, err } = event.payload;
if (err || !logs) return;

if (logs.length >= SolanaWatcher.triggerThreshold) {
const hash = generateSignalHash(event);
logSignal({
agent: SolanaWatcher.name,
type: "high_activity_detected",
glyph: SolanaWatcher.glyph,
hash,
timestamp: new Date().toISOString(),
details: {
signature,
logCount: logs.length,
slot,
preview: logs.slice(0, 3),
},
});

SolanaWatcher.lastSignal = hash;
}
},

getMemory: () => [
SolanaWatcher.lastSignal || "no_signals_yet",
`lastFetched_${lastFetched}`,
`watching_${WATCH_PROGRAM.toBase58()}`,
],

cleanup: async () => {
if (pollHandle) {
clearInterval(pollHandle);
console.log(`[${SolanaWatcher.name}] Polling stopped.`);
}
},
};

Loading