contributor-reward
$
npx mdskill add aaronjmars/aeon/contributor-reward> **${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.
SKILL.md
.github/skills/contributor-rewardView on GitHub ↗
---
name: contributor-reward
description: Closes the contributor flywheel — turns the weekly fork-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 `fork-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 fork-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/fork-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/fork-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/fork-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 (`fork-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
```json
// memory/state/contributor-reward-state.json
{
"weeks": {
"2026-W17": {
"written_at": "2026-04-26T09:00:00Z",
"label": "contributors-2026-W17",
"leaderboard_file": "articles/fork-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):
```yaml
# memory/distributions.yml
defaults:
token: USDC
amount: "5"
chain: base
lists:
```
Compute the new list block:
```yaml
contributors-${TARGET_WEEK}:
description: "Weekly contributor rewards for ${TARGET_WEEK} (auto-generated from fork-contributor-leaderboard)"
token: USDC
amount: "5"
recipients:
- handle: "@alice_dev"
amount: "25" # rank 1
- handle: "@bob_builder"
amount: "20" # rank 2 + first-PR bonus
- handle: "@carol_eng"
amount: "10" # rank 3
- handle: "@dave_ops"
amount: "5" # rank 4
```
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 (`fork-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.
More from aaronjmars/aeon
- [REPLACE: SKILL_NAME]Daily digest of the most interesting new posts on [REPLACE: TOPIC] from RSS feeds and the open web
- Action Converter5 concrete real-life actions for today, leverage-scored against open loops with specificity and anti-fluff gates
- Agent BuzzCurated AI-agent tweets, clustered into narratives with insight summaries
- agent-displacementWeekly tracker of AI agent substitution signals — which roles, companies, and industries show real headcount displacement. Named roles + real deployments only.
- AI Framework WatchWeekly competitive-intelligence digest on the AI agent framework space — momentum, releases, breaking changes across a curated watchlist
- AIXBT PulseCross-domain market pulse from AIXBT's free grounding endpoint — crypto, macro, tradfi, geopolitics. Refreshes taxonomy references (clusters, chains) as a bonus.
- api-health-probeDaily pre-batch API provider health check — detects credit exhaustion or auth failure for every configured provider key before the morning batch runs, giving the operator a window to act before skills degrade
- Approval AuditList a wallet's live ERC-20 token approvals on Base and flag unlimited / risky spender grants. Keyless via Base RPC (eth_getLogs + eth_call) — no explorer key needed.
- article-queueWeekly article idea synthesizer — ranks signals from topic-momentum, beat-tracker, and narrative-tracker into a prioritized queue the article skill reads on next run
- atrium-catalog-watcherWeekly diff of the Atrium marketplace catalog at https://atriumhermes.tech/.well-known/skills/index.json against the prior snapshot — surfaces newly-published skills, removed skills, and updated descriptions. Supply-side complement to sparkleware-catalog (curated skill-packs.json registry) and skill-update-check (version drift of installed skills).