| name | contributor-reward |
| description | Closes the contributor flywheel — turns the contributor-leaderboard ranking into a tier-priced rewards plan, writes it into memory/distributions.yml, and hands off to distribute-tokens for the actual on-chain send |
| var | |
| tags | ["community","crypto"] |
${var} — Optional override. Pass dry-run to print the plan without writing to memory/distributions.yml or sending a notification. Pass an explicit ISO week (e.g. 2026-W17) to force-process that week instead of the most recent leaderboard. Empty = process the most recent leaderboard.
Today is ${today}. Closes the loop from contributor-leaderboard to distribute-tokens: read this week's contributor ranking, price each eligible contributor against a tier table, write a labelled list into memory/distributions.yml, and notify the operator with a one-command run line. Humans still gate the actual send (distribute-tokens execution stays a manual or chained step) — this skill's job is plan generation, not money movement.
Why this design
The contributor-leaderboard already names the people moving the project. The distribute-tokens skill already moves tokens with idempotency, balance preflight, and dry-run. The gap was the wiring between them: a contributor's score on Sunday's leaderboard had no path to a wallet credit. This skill is the wiring — and only the wiring.
It deliberately stops short of executing transfers because (a) distribute-tokens is the only sanctioned transfer path and re-implementing it here would fragment the idempotency state, and (b) keeping a human-visible diff on memory/distributions.yml between plan and execution is the cheapest possible audit trail when real money is involved. The plan lands in git; the operator (or a chained step) runs distribute-tokens contributors-${ISO_WEEK} next.
Tier pricing
| Rank in leaderboard | Reward (USDC) |
|---|
| 1 | 25 |
| 2 | 15 |
| 3 | 10 |
| 4 | 5 |
| 5 | 5 |
First-PR bonus: +5 USDC, additive, applied once-ever per login (tracked in state file). Rewards landing your first merged upstream PR — the highest-leverage signal in the leaderboard scoring.
Eligibility floor: score ≥ 10 AND the contributor must own a non-empty @handle (logins without @ prefix in the table are skipped — bots and parsing artifacts).
Default token: USDC on Base, matching distribute-tokens defaults. Operator can override per-recipient amounts in memory/distributions.yml after the plan is written if a special bonus is warranted.
Config
No new config files. Reads:
articles/contributor-leaderboard-${MOST_RECENT}.md — the source-of-truth ranking
memory/state/contributor-reward-state.json — idempotency + first-PR-bonus history (created on first run)
memory/distributions.yml — the file distribute-tokens reads (created/updated by this skill)
No new secrets. No new external API calls. No curl. All work is local file I/O plus one optional gh api for the upstream default branch.
Steps
1. Parse var and resolve week
- If
${var} starts with dry-run, set MODE=dry-run. Strip the dry-run prefix; remainder (if any) is treated as the week override.
- Otherwise
MODE=execute.
- If the remaining var matches
^\d{4}-W\d{2}$, set TARGET_WEEK=${var} and LEADERBOARD_GLOB="articles/contributor-leaderboard-*.md" (will pick the latest file regardless of date — operator is asserting they know which file maps to that week).
- Otherwise compute
TARGET_WEEK from today: TARGET_WEEK=$(date -u +%G-W%V) (ISO-8601 week-numbering year + week — %G/%V not %Y/%U, so Monday-anchored weeks roll over correctly across years).
2. Find and validate the source leaderboard article
LEADERBOARD_FILE=$(ls -1t articles/contributor-leaderboard-*.md 2>/dev/null | head -1)
- If no file → log
CONTRIBUTOR_REWARD_NO_LEADERBOARD to memory/logs/${today}.md, exit silently (no notify). The leaderboard skill is the upstream dependency; if it didn't run, this skill has nothing to do.
- Compute the file's age in days from its filename suffix (
contributor-leaderboard-YYYY-MM-DD.md). If age > 8 days → log CONTRIBUTOR_REWARD_STALE_LEADERBOARD — last leaderboard ${age}d old, notify the operator that the upstream leaderboard hasn't run, exit. Don't reward against a fortnight-old ranking.
3. Parse the Top Contributors table
The leaderboard's ## Top Contributors section uses this column layout (from the upstream skill spec):
| Rank | Contributor | Score | Merged PRs | Open PRs | Reviews | Fork Commits | New Skills | First PR? | Change |
Extract rows with a tolerant regex: ^\|\s*(\d+)\s*\|\s*@(\S+)\s*\|\s*(\d+)\s*\|.*?\|\s*(✨|—|\s*)\s*\|\s*[^|]*\|\s*$
Capture: rank, login (without @), score, first_pr_marker (✨ if present, else absent).
If zero rows extracted (leaderboard format drift) → log CONTRIBUTOR_REWARD_PARSE_FAIL — extracted 0 rows from ${LEADERBOARD_FILE}, notify with the file path so the operator can inspect, exit.
4. Load idempotency state
{
"weeks": {
"2026-W17": {
"written_at": "2026-04-26T09:00:00Z",
"label": "contributors-2026-W17",
"leaderboard_file": "articles/contributor-leaderboard-2026-04-26.md",
"rewards": [
{ "login": "alice_dev", "rank": 1, "score": 47, "amount": "25", "first_pr_bonus": false },
{ "login": "bob_builder", "rank": 2, "score": 31, "amount": "20", "first_pr_bonus": true }
]
}
},
"first_pr_bonus_paid": ["bob_builder", "carol_eng"]
}
Bootstrap with {"weeks": {}, "first_pr_bonus_paid": []} if the file doesn't exist.
5. Compute the plan
For each parsed row with rank ≤ 5 AND score ≥ 10:
- Look up
base_amount from the tier table (rank 1→25, 2→15, 3→10, 4-5→5).
- If
first_pr_marker == "✨" AND login ∉ first_pr_bonus_paid → set first_pr_bonus = true, amount = base_amount + 5. Otherwise first_pr_bonus = false, amount = base_amount.
- Build row:
{ rank, login, score, base_amount, first_pr_bonus, amount }.
If weeks[TARGET_WEEK] already exists in state → this week was already processed. Diff the current plan against state.weeks[TARGET_WEEK].rewards keyed on login:
- If diffs are empty (same logins, same amounts) → log
CONTRIBUTOR_REWARD_ALREADY_PROCESSED — week ${TARGET_WEEK}, exit silently (no notify). Idempotent re-run.
- If diffs exist (leaderboard re-ran after first reward write — late tweet bumped a score, etc.) → flag
RE_PROCESS. Continue but don't re-pay anyone already in state.weeks[TARGET_WEEK].rewards; add only the deltas. New entries get full reward; existing entries with bumped amounts get the delta (e.g. moved from rank 3→2 = additional 5 USDC top-up). Demoted entries are not clawed back.
If the plan is empty (zero eligible contributors after threshold + dedup) → log CONTRIBUTOR_REWARD_NO_ELIGIBLE and exit silently.
6. Render the plan
Contributor Reward Plan — ${TARGET_WEEK} (${MODE})
Source: ${LEADERBOARD_FILE}
Tier: rank 1=25, 2=15, 3=10, 4-5=5 USDC; first-PR bonus +5 once per login.
✓ #1 @alice_dev score 47 → 25 USDC [NEW]
✓ #2 @bob_builder score 31 → 20 USDC (15 + 5 first-PR)[NEW + BONUS]
✓ #3 @carol_eng score 24 → 10 USDC [NEW]
✓ #4 @dave_ops score 18 → 5 USDC [NEW]
↻ #5 @eve_hax score 14 → 5 USDC [DEDUP — already in state]
Total to write: 60 USDC across 4 new entries.
Total in state for ${TARGET_WEEK} after write: 5 entries, 65 USDC.
Next: ./aeon distribute-tokens "dry-run:contributors-${TARGET_WEEK}" (preview)
./aeon distribute-tokens "contributors-${TARGET_WEEK}" (execute)
If MODE=dry-run: notify this plan with header *Contributor Reward Plan — ${TARGET_WEEK}* — DRY RUN, log to memory/logs/${today}.md, exit CONTRIBUTOR_REWARD_DRY_RUN. Do not touch memory/distributions.yml or the state file.
7. Update memory/distributions.yml
Read memory/distributions.yml. If missing → bootstrap with the standard header (matching distribute-tokens' bootstrap style):
defaults:
token: USDC
amount: "5"
chain: base
lists:
Compute the new list block:
contributors-${TARGET_WEEK}:
description: "Weekly contributor rewards for ${TARGET_WEEK} (auto-generated from contributor-leaderboard)"
token: USDC
amount: "5"
recipients:
- handle: "@alice_dev"
amount: "25"
- handle: "@bob_builder"
amount: "20"
- handle: "@carol_eng"
amount: "10"
- handle: "@dave_ops"
amount: "5"
Recipient ordering matches plan order (rank ascending). Per-recipient amount is required so distribute-tokens picks up the tier-priced value rather than falling back to the list default.
Update strategy:
- If a list named
contributors-${TARGET_WEEK} already exists in the YAML, replace it wholesale (the plan is the authoritative current state).
- Otherwise append the block under
lists: (preserving existing lists — never rewrite them).
- Use a YAML-aware update (e.g.
python -c "import yaml; ..." if available, otherwise a careful text-based block replacement keyed on the ^ contributors-${TARGET_WEEK}:$ line). If YAML parse fails on the existing file → log error, do not write, notify the operator (file is hand-edited; auto-edit would clobber).
Verify the write by re-reading the file and confirming the list is present and has len(recipients) == len(plan).
8. Update state file
Atomically write the updated state JSON to memory/state/contributor-reward-state.json:
- Set
weeks[TARGET_WEEK] = { written_at: now_utc, label, leaderboard_file, rewards: [{login, rank, score, amount, first_pr_bonus}, ...] } (full replacement on RE_PROCESS, otherwise additive).
- Append any logins where
first_pr_bonus == true to first_pr_bonus_paid (deduplicated).
Write to a tempfile and mv over the target so partial writes can't corrupt state.
9. Notify
*Contributor Reward Plan — ${TARGET_WEEK}*
Wrote ${N_NEW} new entries (${TOTAL_USDC} USDC) to memory/distributions.yml as `contributors-${TARGET_WEEK}`.
Top of plan:
#1 @alice_dev — 25 USDC
#2 @bob_builder — 20 USDC (✨ first-PR bonus)
#3 @carol_eng — 10 USDC
#4 @dave_ops — 5 USDC
${IF_DEDUP}
Source: ${LEADERBOARD_FILE}
First-PR bonuses awarded: ${LIST_OR_NONE}
Next: run `distribute-tokens dry-run:contributors-${TARGET_WEEK}` to preview, then drop the `dry-run:` prefix to execute.
Plan: https://github.com/${GITHUB_REPOSITORY}/blob/main/memory/distributions.yml
Suppress the ${IF_DEDUP} line when no entries were deduped. Use $GITHUB_REPOSITORY env var for the link target. Send via ./notify.
Significance gate: notify only when N_NEW ≥ 1. Re-process runs that produced zero new entries (RE_PROCESS with all rewards already paid) → silent log only.
10. Log
Append to memory/logs/${today}.md:
## Contributor Reward
- **Mode:** execute | dry-run | already-processed | no-leaderboard | stale-leaderboard | parse-fail | no-eligible
- **Week:** ${TARGET_WEEK}
- **Source:** ${LEADERBOARD_FILE} (age: ${AGE_DAYS}d)
- **List label:** contributors-${TARGET_WEEK}
- **Entries written (new):** ${N_NEW}
- **Entries deduped:** ${N_DEDUP}
- **Total USDC planned:** ${TOTAL_USDC}
- **First-PR bonuses:** [list or "none"]
- **Notification sent:** yes/no
Exit codes
CONTRIBUTOR_REWARD_OK — plan written, notification sent
CONTRIBUTOR_REWARD_DRY_RUN — plan rendered, no writes, notification sent
CONTRIBUTOR_REWARD_ALREADY_PROCESSED — week already in state with identical plan, silent exit
CONTRIBUTOR_REWARD_NO_LEADERBOARD — no leaderboard article found, silent exit
CONTRIBUTOR_REWARD_STALE_LEADERBOARD — leaderboard >8 days old, notified
CONTRIBUTOR_REWARD_PARSE_FAIL — could not extract any rows from the leaderboard table, notified
CONTRIBUTOR_REWARD_NO_ELIGIBLE — zero contributors above threshold, silent exit
CONTRIBUTOR_REWARD_ERROR — file I/O or YAML write failure, notified
Sandbox note
Pure local file I/O — no curl, no auth-bearing headers, no env-var-expansion. Reads articles/, memory/state/, memory/distributions.yml. Writes memory/state/contributor-reward-state.json, memory/distributions.yml, memory/logs/${today}.md. The optional gh api call for the upstream default branch isn't needed at runtime — distribute-tokens does its own resolution. No prefetch, no postprocess scripts required.
Constraints
- Money-adjacent — never auto-execute. This skill writes plans and notifies. The actual transfer always goes through
distribute-tokens, which has its own preflight and idempotency. Do not call distribute-tokens from inside this skill.
- Idempotency is per-(week, login). Re-runs in the same week add only deltas; demotions never claw back already-paid amounts.
- First-PR bonus is once-ever per login. Track in
first_pr_bonus_paid list; never re-award even if the same person appears as ✨ on a later leaderboard (which they shouldn't, since ✨ means first ever — but defend against parsing drift).
- No silent overwrites of distributions.yml. If the file exists and is malformed, fail loudly rather than rewriting.
- Eligibility floor stays low (score ≥ 10) by design. A single merged upstream PR (+10) qualifies — the goal is to reward shipped work, not gate on volume.
Future iterations
- Wire as a chain (
contributor-leaderboard → contributor-reward → distribute-tokens dry-run) once the operator is comfortable with end-to-end automation. The pieces exist; the chain wiring is a one-line aeon.yml change.
- Add a Bankr Agent API "wallet-linked?" pre-filter so contributors without linked wallets are flagged in the notification (prevents distribute-tokens from logging RESOLVE_FAILED rows on every run).
- Tier table should become operator-configurable via
memory/contributor-reward-config.yml once the first month of runs reveals the right curve. Hardcoded for v1.
Write the full plan. No TODOs or placeholders.