Rust service that watches the Livepeer protocol explorer API, persists event state in SQLite, and delivers Discord notifications.
The app has two deployment shapes:
webhook-onlymode posts public-channel payout digests and daily/weekly/monthly network summaries to a Discord webhook.commands-enabledmode adds a Discord bot user, slash commands, per-user orchestrator subscriptions, reward DMs, delegator-activity digest DMs, reward-cut / fee-share change DMs, and the reward-call watch (pending/missed reward DMs plus a public delinquency digest).
Detailed architecture lives in docs/design-docs/architecture.md. Core repo rules live in docs/design-docs/core-beliefs.md.
The bot consumes the explorer API only. It does not talk to chain nodes, subgraphs, or external cron.
Core responsibilities:
- Poll
WinningTicketRedeemedevents and post orchestrator payout digests. - Post closed-period network summaries for daily, weekly, and monthly windows.
- Optionally expose Discord slash commands for subscribing to orchestrators.
- Optionally DM subscribers about
Reward,Bond,Unbond,Rebond, andTranscoderUpdate(reward-cut / fee-share change) activity. - Optionally watch reward calls per round: DM subscribers when a subscribed orchestrator has not called reward as the round progresses, post a public digest of all delinquent active orchestrators when the round locks, and DM a final missed-reward notice after the round closes.
- Persist cursors, dedup state, delivery watermarks, and subscription data in SQLite.
- Typed explorer boundary: upstream JSON is parsed into generated Rust types in
src/domains/explorer/types.rs. - Strict startup validation: env vars are parsed once in src/config.rs; bad config fails fast.
- Append-only persistence: migrations are additive, cursors are explicit, and delivery flags are written after successful sends.
- Contract-locked embeds: webhook and DM payloads are snapshot-tested in tests/embeds.rs.
- Architecture guardrails: domain import rules are enforced in tests/architecture.rs.
- Optional interactive mode: slash commands, DM delivery, the reward-call watch, and cold-start seeding are enabled only when
COMMANDS_ENABLED=true. - Optional observability: a Prometheus
/metrics+/healthendpoint is served whenMETRICS_BINDis set.
.
├── AGENTS.md # repository map and “read next” guide
├── README.md # operator/developer entrypoint
├── Cargo.toml # crate metadata and pinned dependencies
├── Dockerfile # container build
├── migrations/ # append-only SQLite schema
├── infra/ # compose + image build helpers
├── docs/
│ ├── design-docs/ # architecture and invariants
│ ├── product-specs/ # exact message/embed contracts
│ ├── generated/ # vendored OpenAPI input
│ └── exec-plans/ # completed implementation plans
├── src/
│ ├── main.rs # process bootstrap and tracing init
│ ├── config.rs # env parsing and validation
│ ├── runtime.rs # object graph + task spawning
│ ├── seed.rs # cross-domain delegator-history seeding
│ ├── providers/ # HTTP, DB, Discord clients, gateway runtime
│ └── domains/
│ ├── explorer/ # typed REST client and API boundary
│ ├── state/ # SQLite repos for public bot state
│ ├── subscriptions/ # SQLite repo for user subscriptions
│ ├── notify/ # webhook and DM payload builders
│ ├── scheduler/ # pollers and posters
│ └── commands/ # slash command handlers
└── tests/ # structural and snapshot tests
- Parse at the boundary. External bytes become typed structs before business logic touches them.
- Domains are stratified.
explorerandsubscriptionsare strict leaves;statemay only importexplorer::types; composition belongs inscheduler,commands,seed.rs, andruntime.rs. - Product docs are contracts. If embed output changes, docs/product-specs/messages.md and snapshot tests must change in the same PR.
- Startup is strict. There is no silent degraded mode for bad config, missing migrations, or failed boot wiring.
- Migrations are append-only. Never edit an already-deployed migration.
Requirements:
- Rust
1.95via rust-toolchain.toml - SQLite via
sqlxruntime linkage only; no separate local DB service is required
Local build and test:
cargo fmt --check
cargo test
cargo build --releaseRun locally:
cp .env.example .env
# fill required values
cargo run --releaseContainer build:
docker build -t livepeer-payout-bot .Compose-based run:
cp infra/.env.example infra/.env
docker compose -f infra/docker-compose.yaml up -dFor containerized runs, keep DATABASE_URL on the mounted /data volume, for
example sqlite:///data/livepeer-payout-bot.db. A relative SQLite path such as
sqlite://./livepeer-payout-bot.db lives inside the container filesystem and
will appear to "lose" subscriptions and cursors after container replacement.
The full env var contract is documented in .env.example. The key variables are:
Required in all modes:
EXPLORER_BASE_URL: Livepeer protocol explorer base URL.DISCORD_WEBHOOK_URL: Discord webhook(s) for public digest and summary embeds. Accepts a single URL, or several comma-separated URLs to fan the same posts out to multiple servers (one webhook per server channel). Delivery is best-effort per webhook and all servers share one global send watermark, so a permanently-broken webhook silently misses messages until fixed rather than blocking or duplicating to the healthy ones.DATABASE_URL: SQLite connection string.
Optional timing and transport knobs:
EVENT_POLL_INTERVAL_SECSDIGEST_WINDOW_SECSDIGEST_FETCH_LIMITSUMMARY_POLL_INTERVAL_SECSSUMMARY_SETTLE_DAILY_SECS/SUMMARY_SETTLE_WEEKLY_SECS/SUMMARY_SETTLE_MONTHLY_SECS/SUMMARY_MAX_DEFER_SECS(summary readiness gating; see.env.example)HTTP_TIMEOUT_SECSRUST_LOGUSER_AGENTMETRICS_BIND(e.g.0.0.0.0:9300; serves Prometheus/metrics+/health, disabled when unset)
Optional safety flag:
WEBHOOK_POST_ENABLED(defaulttrue): when set tofalse, the bot still polls and persists events but does not spawndigest_posterorsummary_poster, so nothing is sent toDISCORD_WEBHOOK_URL. Use it in a dev process that shares its webhook URL with prod to avoid double-posting. Flipping back totruedrains the backlog at the next digest boundary.
Additional variables when COMMANDS_ENABLED=true:
DISCORD_BOT_TOKENDISCORD_APPLICATION_IDDISCORD_GUILD_IDMAX_SUBSCRIPTIONS_PER_USERDM_FAILURE_AUTO_UNSUBREWARD_POLL_INTERVAL_SECSDELEGATOR_POLL_INTERVAL_SECSCUT_CHANGE_POLL_INTERVAL_SECSSUBSCRIBER_DIGEST_INTERVAL_SECSREWARD_WATCH_ENABLED/REWARD_WATCH_POLL_INTERVAL_SECS/REWARD_WATCH_FIRST_ALERT_PCT/REWARD_WATCH_REALERT_STEP_PCT/REWARD_WATCH_DIGEST_PCT/ROUND_LENGTH_BLOCKS(reward-call watch; see.env.example)
src/runtime.rs constructs shared providers once, then spawns long-lived Tokio tasks:
- Always on:
event_pollerdigest_postersummary_poster
- Only when commands are enabled:
- startup delegator-history seed
reward_pollerdelegator_pollercut_change_pollersubscriber_digest_posterreward_watch_poller(unlessREWARD_WATCH_ENABLED=false)- Discord gateway / slash command runtime
- Detached (never fatal): the Prometheus
/metrics+/healthserver, spawned only whenMETRICS_BINDis set.
The process exits on SIGINT/SIGTERM or when one of the always-on webhook
tasks dies unexpectedly. In commands-enabled mode, the Discord gateway is
treated as non-critical and is restarted in-process so slash-command trouble
does not take down payout digests or summaries.
- Repo map: AGENTS.md
- Detailed architecture: docs/design-docs/architecture.md
- Operating rules: docs/design-docs/core-beliefs.md
- Embed contract: docs/product-specs/messages.md
- Upstream API contract input: docs/generated/openapi.json
Licensed under the MIT License.