Guidance for AI agents working on this codebase.
Standalone CLI relayer node for VeBetterDAO. Casts auto-votes and claims rewards on behalf of users on the VeChain blockchain, earning relayer fees.
Not part of the b3tr monorepo. This is a self-contained Node.js project.
- TypeScript compiled to CommonJS (
tsc) @vechain/sdk-core+@vechain/sdk-networkfor blockchain interaction@vechain/vebetterdao-contractsfor contract ABIs (typechain factories)chalkv4 (CJS) for terminal colors- Node.js 20+
src/
├── index.ts # Entry point. Parses env, derives wallet, runs main loop.
│ # Each cycle: fetchSummary → render → castVotes → claimRewards → render → sleep.
├── config.ts # Mainnet + testnet-staging contract addresses. No external config deps.
├── contracts.ts # All on-chain reads. Wraps thor.contracts.executeCall for view functions.
│ # Also has getAutoVotingUsers() which paginates AutoVotingToggled events.
├── relayer.ts # Transaction execution. processBatch() handles multi-clause txs with
│ # gas simulation, failure isolation, and retry. Exports runCastVoteCycle
│ # and runClaimRewardCycle.
├── display.ts # Terminal rendering. renderSummary() draws the box UI.
│ # renderCycleResult() formats cycle outcomes for the activity log.
└── types.ts # NetworkConfig, RelayerSummary, CycleResult, LogFn interfaces.
All view functions go through contracts.ts:call() which wraps thor.contracts.executeCall() and returns res.result.array. The ABIContract type is any in the signature to avoid generic type issues with the SDK.
getAutoVotingUsers() fetches AutoVotingToggled events from block 0 to a snapshot block in pages of 1000 (Thor node limit). Builds a Map<address, boolean> and returns only addresses where the final state is enabled=true.
processBatch() in relayer.ts splits users into configurable-size batches. For each batch:
- Build clauses via
Clause.callFunction() - Estimate gas - if reverted, isolate failures individually
- Sign with
Transaction.of(body).sign()and send - Wait for receipt
isolateAndRetry() tests each clause individually to separate permanent failures (e.g. already voted) from transient ones (RPC errors), then retries the valid set.
Before voting, we check hasVoted for all users in chunks of 50 to avoid overwhelming the node with concurrent RPC calls.
npm install # Install deps
npm run build # tsc → dist/
npm start # Run compiled (node dist/index.js)
npm run dev # Run with ts-node (no build)MNEMONICorRELAYER_PRIVATE_KEY(required, one of)RELAYER_NETWORK—mainnet|testnet-staging(default: testnet-staging)NODE_URL— override Thor node URLBATCH_SIZE— users per tx batch (default: 50)DRY_RUN—1to simulate without sending txsPOLL_INTERVAL_MS— ms between cycles (default: 300000, min: 60000)RUN_ONCE—1to exit after one cycle
MNEMONIC and RELAYER_PRIVATE_KEY also support Docker Compose secrets as a fallback. The envOrSecret() helper in index.ts checks the env var first, then reads /run/secrets/<name>. Secret names: mnemonic, relayer_private_key.
XAllocationVoting: currentRoundId, roundSnapshot, roundDeadline, isActive, getTotalAutoVotingUsersAtRoundStart, totalVoters, totalVotes, hasVotedRelayerRewardsPool: getRegisteredRelayers, isRegisteredRelayer, getTotalRewards, claimableRewards, isRewardClaimable, totalActions, completedWeightedActions, totalWeightedActions, getMissedAutoVotingUsersCount, getVoteWeight, getClaimWeight, getRelayerFeePercentage, getRelayerFeeDenominator, getFeeCap, getEarlyAccessBlocks, totalRelayerActions, totalRelayerWeightedActions
XAllocationVoting.castVoteOnBehalfOf(address voter, uint256 roundId)— cast vote for auto-voting userVoterRewards.claimReward(uint256 cycle, address voter)— claim reward for user (fee goes to RelayerRewardsPool)
XAllocationVoting.AutoVotingToggled(address account, bool enabled)— paginated from block 0 to snapshot
Add a new NetworkConfig in config.ts with contract addresses, then add a case in getNetworkConfig().
- TypeScript strict mode
- No class-based architecture — pure functions and interfaces
- Minimal dependencies
anyused sparingly for SDK type compatibility (ABIContract generics)