Standalone service that runs weekly and checks whether Agent0 web, A2A, and MCP endpoints are reachable. If reachable, it posts a public, on-chain ERC-8004 feedback message advertising reachability.
This is designed to be low-maintenance and deployed as AWS Lambda + EventBridge schedule, with CloudWatch observability and DynamoDB idempotency.
This repo is multi-role: Reachability is implemented today, and additional watchtowers (e.g. domain verification) can be deployed as separate Lambda functions from the same
watchtower/codebase/bundle.
- Weekly, for each configured chain:
- Load agents from the chain’s Agent0 subgraph (SDK defaults).
- Extract each agent’s
registrationFile.webEndpoint,registrationFile.a2aEndpoint, andregistrationFile.mcpEndpoint. - Probe reachability (HTTP-level).
- If reachable, post ERC-8004 feedback on-chain:
- agentId = the agent being checked (
chainId:agentId) - clientAddress = the watchtower wallet address
- tag1 =
reachable - tag2 =
webora2aormcp - value =
1 - valueDecimals =
0 - endpoint = full normalized URL (e.g.
https://endpoint.com/path)
- agentId = the agent being checked (
- Horizontal scaling / fan-out (single scheduled Lambda invocation is intended).
- Protocol-level “deep” A2A/MCP handshakes (optional future improvement).
For each reachable endpoint, the watchtower posts a feedback entry. Consumers can search for these tags in the subgraph to validate “reachable” endpoints publicly.
If enabled, the watchtower only scans agents that have already received feedback satisfying:
tag1 = "STAR"clientAddress = <STAR_REVIEWER_ADDRESS>isRevoked = false
This lets you curate which agents are scanned without maintaining a separate allowlist file.
The watchtower stores a record keyed by:
(roleId, chainId, agentId, endpointType, hostname)
If a success was recorded within the last IDEMPOTENCY_WINDOW_DAYS (default 7), the watchtower skips posting again.
-
Controller/engine (
src/runtime/engine.ts+ role plugins undersrc/roles/)- Loops over configured chains
- Optional STAR allowlist query (per chain)
- Fetch agents + endpoints from subgraph
- Runs reachability checks with bounded concurrency
- Posts feedback and writes idempotency records
-
Subgraph ingestion (
src/subgraph/agents.ts)- Uses a direct GraphQL query to fetch
agents { id registrationFile { webEndpoint mcpEndpoint a2aEndpoint } } - Uses a direct GraphQL query for STAR gating (exact
tag1="STAR"match)
- Uses a direct GraphQL query to fetch
-
Reachability probe (
src/checks/reachability.ts)- Normalizes URL (defaults scheme to
https://if missing) - Uses
HEADthenGETfallback - Treats
2xx/3xx/401/403as reachable (server is alive, even if auth required)
- Normalizes URL (defaults scheme to
-
Feedback posting (
src/feedback/post.ts)- Uses the
agent0-sdkTypeScript SDK to prepare and submit feedback on-chain
- Uses the
-
Idempotency store (
src/storage/idempotency.ts)- DynamoDB get/put for last success time and last tx reference
flowchart TD
eventBridge[EventBridgeSchedule] --> controller[WatchtowerController]
controller --> subgraph[Agent0SubgraphGraphQL]
controller --> endpoints[AgentEndpoints_HTTP]
controller --> rpc[RPC_provider]
rpc --> chain[ReputationRegistry_giveFeedback]
controller --> db[DynamoDB_idempotency]
- Node.js 20.x Lambda runtime recommended.
- Lambda function (scheduled weekly by EventBridge)
- EventBridge rule (cron schedule)
- DynamoDB table for idempotency
- Secrets Manager secret containing the watchtower private key
- CloudWatch logs + CloudWatch alarms
- SNS topic + email subscription (or SES) for notifications
This repo includes CloudFormation/SAM templates under
watchtower/deploy/(see that folder once built).
watchtower/deploy/alarms.yaml provisions:
- an SNS topic with an email subscription
- alarms on:
- Lambda
Errors - Lambda
Duration - custom EMF metric
FeedbackFailed(namespaceAgent0/Watchtower)
- Lambda
All configuration is via environment variables.
CHAINS: Comma-separated chain IDs to scan (e.g.11155111,84532)RPC_URL_<chainId>: RPC URL per chain (e.g.RPC_URL_11155111,RPC_URL_84532)WATCHTOWER_PRIVATE_KEY_SECRET_ID: Secrets Manager secret id/arn with the private keyDDB_TABLE_NAME: DynamoDB idempotency table name
STAR_MODE_ENABLED:true|false(defaultfalse)STAR_REVIEWER_ADDRESS: required if STAR mode is enabledIDEMPOTENCY_WINDOW_DAYS: default7MAX_CONCURRENCY: default10REQUEST_TIMEOUT_MS: default8000PROBE_RETRIES: default1
Supported secret value formats:
- Plaintext:
0xabc123...orabc123...
- JSON:
{ "privateKey": "0xabc123..." }
From watchtower/:
npm install
npm run build
node dist/index.jsFor Lambda, the handler entrypoint is:
dist/reachability.js(export:handler) ✅ recommendeddist/domainVerification.js(export:handler) (currently safe no-op)dist/lambda/handler.js(export:handler) (backwards-compatible default)