A WAVS demo that pays GitHub contributors on-chain. Every N blocks, a WASM component fetches activity from a configured repository, scores contributors, builds a Merkle tree of cumulative rewards, and submits the root on-chain. Contributors claim tokens directly with a Merkle proof — no intermediary, no manual distribution.
- A contributor creates a public GitHub Gist containing
wavs-github-rewards eth: 0x<their-address>and callsregisterIdentity()on-chain - The WAVS block-interval trigger fires every ~50,400 blocks (~7 days on mainnet, faster on testnets)
- The WASM component fetches merged PRs, closed issues, review comments, and new stars since
lastSnapshotTimestamp - Each contributor is scored, their GitHub Gist is verified to match the registered address, and new points are added to their cumulative total
- A Merkle tree is built from cumulative allocations, uploaded to IPFS, and the root is submitted on-chain via the WAVS aggregator
- Contributors call
claim()with a Merkle proof to receive their tokens
Scoring weights (configurable in config/components.json):
| Activity | Points |
|---|---|
| PR merged | 100 |
| Issue resolved | 50 |
| Review comment | 25 |
| Issue filed | 10 |
| Star given | 5 |
Each point is worth 0.001 ETH of reward tokens (tokens_per_point = 1e15 wei).
| Contract | Description |
|---|---|
GitHubIdentityRegistry |
Maps GitHub usernames → ETH addresses via public Gist verification |
GitHubRewardToken |
ERC-20 backed by ETH (1:1 mint); used as the reward token |
GitHubRewardDistributor |
Receives WAVS-signed Merkle roots; extends Morpho's UniversalRewardsDistributor for claims |
- Foundry
- Rust + cargo-component
- A running WAVS node (see WAVS docs)
- A GitHub Personal Access Token with
repo:readaccess - A Pinata API key for IPFS pinning
npm installexport WAVS_ENV_GITHUB_TOKEN=ghp_... # GitHub PAT (repo:read)
export WAVS_ENV_PINATA_API_KEY=... # Pinata API key for IPFS
export GITHUB_REPO_OWNER=Lay3rLabs # Repository owner
export GITHUB_REPO_NAME=WAVS # Repository name
export FUNDED_KEY=0xac0974... # Deployer private key
export RPC_URL=http://localhost:8545 # RPC endpoint
export SERVICE_MANAGER_ADDR=0x... # WAVS ServiceManager addressforge script script/Deploy.s.sol:DeployScript \
--sig "run(string)" "$SERVICE_MANAGER_ADDR" \
--rpc-url $RPC_URL --broadcastThis deploys GitHubIdentityRegistry, GitHubRewardToken, and GitHubRewardDistributor, seeds the distributor with 100 ETH of reward tokens, and writes addresses to .docker/github_rewards_deploy.json.
cargo component build --release -p github-rewardsOutput: target/wasm32-wasip2/release/github_rewards.wasm
wavs-cli service deploy \
--config config/components.json \
--wasm-dir target/wasm32-wasip2/releaseThe component will fire on the configured block interval and begin submitting snapshots.
Anyone who wants to receive rewards must link their GitHub username to an ETH address:
Step 1: Create a public GitHub Gist with any filename containing this line:
wavs-github-rewards eth: 0x<your-eth-address>
Step 2: Register on-chain (using cast or your preferred tool):
cast send $IDENTITY_REGISTRY_ADDR \
"registerIdentity(string,string)" \
"your-github-username" "your-gist-id" \
--private-key $YOUR_KEY --rpc-url $RPC_URLThe Gist is verified on every snapshot cycle — if it's missing or points to a different address, that contributor is skipped for that period.
After a snapshot is submitted, generate a Merkle proof from the IPFS snapshot and call:
cast send $DISTRIBUTOR_ADDR \
"claim(address,address,uint256,bytes32[])" \
"$YOUR_ADDRESS" "$REWARD_TOKEN" "$CLAIMABLE_AMOUNT" "$PROOF" \
--private-key $YOUR_KEY --rpc-url $RPC_URLThe distributor uses UniversalRewardsDistributor's cumulative claim mechanism — CLAIMABLE_AMOUNT is your total earned across all snapshots. The contract tracks how much you've already claimed.
forge testAll scoring weights, token economics, and the block interval are configured in config/components.json:
{
"trigger": { "block_interval": { "blocks": 50400 } },
"config": {
"values": {
"score_pr_merged": "100",
"score_issue_resolved": "50",
"score_review": "25",
"score_issue_filed": "10",
"score_star": "5",
"tokens_per_point": "1000000000000000"
}
}
}Block trigger
│
▼
WASM component
├─ Read lastSnapshotTimestamp from GitHubRewardDistributor
├─ Read lastIpfsHash → fetch previous snapshot from IPFS
├─ Fetch GitHub activity since last snapshot (PRs, issues, reviews, stars)
├─ Score contributors (skip unregistered or unverified identities)
├─ Merge new scores with previous cumulative totals
├─ Build Merkle tree over [address, token, amount] leaves
├─ Upload snapshot JSON to IPFS via Pinata
└─ Return (merkleRoot, ipfsHash) to WAVS aggregator
│
▼
WAVS aggregator collects operator signatures
│
▼
GitHubRewardDistributor.handleSignedEnvelope()
├─ Validates aggregated signature via SERVICE_MANAGER
├─ Updates Merkle root (via Morpho UniversalRewardsDistributor)
└─ Stores lastSnapshotTimestamp + lastIpfsHash
│
▼
Contributors call claim() with Merkle proof
- Blog post: Paying Developers for Their Work, On-Chain
- WAVS Documentation
- wavs-rewards demo — simpler Merkle rewards without GitHub integration