Skip to main content

distribute-tokens

Two-phase contributor rewards - plan builds a tier-priced payout from the repo's merged-PR ranking; send executes it on-chain via Bankr Wallet API with per-recipient idempotency and dry-run.

Jump to install

Source facts

Repository
aeonfun/aeon
Last source activity
August 4, 2026 at 21:01
Detected SKILL.md language
English
Stars
755
Forks
264

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
distribute-tokens
description
Two-phase contributor rewards - plan builds a tier-priced payout from the repo's merged-PR ranking; send executes it on-chain via Bankr Wallet API with per-recipient idempotency and dry-run.
metadata
{"title":"Distribute Tokens","category":"crypto","var":"","tags":["community","crypto"],"requires":["BANKR_API_KEY?"],"capabilities":["external_api","writes_external_host","onchain_writes","sends_notifications"]}
<!-- autoresearch: variation C — robustness via per-recipient idempotency state, two-phase resolve→execute, dry-run, retries, 403/429 handling, recovery. Merged: contributor-reward's tier-priced reward-computation folded in as the plan/input phase; the on-chain distribution stays the execute phase. --> > **${var}** — Phase + target selector. Grammar: `[plan:|all:][dry-run:]<target>` > - `` (empty) / `<label>` / `dry-run:<label>` → **send** phase: distribute a list from `memory/distributions.yml` (empty = first list). *[default — no prefix]* > - `plan:` / `plan:<week>` / `plan:dry-run` / `plan:dry-run:<week>` → **plan** phase only: compute rewards from the repo's merged PRs and write the list into `memory/distributions.yml`. > - `all:` / `all:<week>` / `all:dry-run` / `all:dry-run:<week>` → **plan then send** in one run. > > `<label>` is a distribution-list label (e.g. `contributors-2026-W17`). `<week>` is an ISO week (`2026-W17`); empty `<week>` = most recent completed ISO week. `dry-run:` previews without side effects (no yml/state writes in plan; no transfers in send). ## Why this design This skill owns the whole contributor flywheel: **who deserves what** (plan) and **moving the money** (send). It is split into two phases that can run independently or chained. **Plan phase — the wiring the project was missing.** Merged PRs already name the people moving the project, but shipped work had no path to a wallet credit. The plan phase is that wiring: it ranks contributors by the PRs they merged in the target week (straight from the GitHub API), prices each eligible contributor against a tier table, and writes a labelled list into `memory/distributions.yml` — the exact file the send phase reads. 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, and the operator (or `all:` mode, or a chained step) runs the send next. **Send phase — this moves real money.** The biggest failure mode is double-sending (re-runs, retries after partial failures, day-rollover bypass of "skip if today" logic) or sending into a black hole (no preflight balance, deprecated API path, missing handle resolution). The send phase therefore: 1. **Persists per-recipient idempotency state** in `memory/state/distributions.json` keyed on `(list, recipient, date_utc)` with the txHash. A successful transfer is *never* re-sent within the same UTC day, even across re-runs or workflow restarts. 2. **Two-phase execution**: RESOLVE (validate config, key, balance, resolve all handles → addresses, build plan) → EXECUTE (send each transfer, persist state after each one). RESOLVE failures abort before any send. 3. **Dry-run mode** outputs the full plan with no transfers. 4. **Wallet API only** for actual transfers — Bankr's docs deprecate the Agent API for transfers. Agent API is used only for handle→address resolution. The two phases stay decoupled by design: the send phase is the only sanctioned transfer path and owns the idempotency state file, so the plan phase never touches transfer state. `all:` mode simply runs plan then send in sequence (plan writes the list, send reads it), so nothing is re-implemented. ## Config Reads two independent config/state surfaces depending on phase: - `memory/distributions.yml` — the distribution lists (read by **send**, written by **plan**). - The **plan** phase computes its ranking live from the repo's merged PRs via the GitHub API — no input file (see Phase A). - `memory/state/distributions.json` — per-recipient send idempotency (read/written by **send**). - `memory/state/contributor-reward-state.json` — plan idempotency + first-PR-bonus history (read/written by **plan**). If `memory/distributions.yml` is missing when the **send** phase needs it, bootstrap with a commented template (see Send Step 1) and exit cleanly with `DISTRIBUTE_TOKENS_OK — bootstrapped distributions.yml; edit and re-run`. ```yaml # memory/distributions.yml defaults: token: USDC # USDC | ETH (Base only) amount: "5" chain: base lists: contributors: description: "Weekly contributor rewards" token: USDC amount: "10" recipients: - handle: "@alice_dev" # Twitter/X — resolved via Bankr Agent API amount: "15" - handle: "@bob_builder" - address: "0x742d...5678" # direct EVM address — preferred path label: "Charlie" amount: "20" ``` ### Required secrets | Secret | Phase | Purpose | |--------|-------|---------| | `BANKR_API_KEY` | send (and any dry-run send, which still preflights) | Bankr API key (`bk_...`). Must be **read-write** with **Wallet API** enabled. Read-only keys → 403. **Not needed for `plan:` (pure local file I/O).** | ### Token addresses on Base - USDC: `0x833589fcd6edb6e08f4c7c32d4f71b54bda02913` - ETH (native): `tokenAddress: "0x0000000000000000000000000000000000000000"`, `isNativeToken: true` ### Tier pricing (plan phase) | 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 `memory/state/contributor-reward-state.json`). 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). A single merged upstream PR (+10) qualifies — the goal is to reward shipped work, not gate on volume. Default `token: USDC` on Base. Operator can override per-recipient amounts in `memory/distributions.yml` after the plan is written if a special bonus is warranted. --- Read `memory/MEMORY.md` and scan the last ~3 days of `memory/logs/` for anything already reported (don't re-report the same signal). ## Step 0 — Parse the selector and dispatch Resolve time anchors up front: `today=$(date -u +%F)` and `today_utc="$today"`. Parse `${var}`: 1. **Phase prefix.** If `${var}` starts with `plan:` → `PHASE=plan`, strip `plan:`. Else if it starts with `all:` → `PHASE=all`, strip `all:`. Else → `PHASE=send` and **do not strip anything** (the remaining legacy grammar is parsed by the send phase itself). 2. **Dry-run.** For `PHASE=plan`/`all`: if the remainder starts with `dry-run` (optionally `dry-run:`), set `MODE=dry-run` and strip that token; else `MODE=execute`. (For `PHASE=send`, the send phase parses `dry-run:` itself — see Send Step 1.) 3. **Target.** - `PHASE=send`: the (unstripped) var is the send target — `dry-run:<label>` or `<label>` or empty. - `PHASE=plan`/`all`: the remainder is an optional `<week>`. If it matches `^\d{4}-W\d{2}$`, set `TARGET_WEEK=<week>`; else compute `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). Dispatch: - `PHASE=plan` → run **Phase A** only. - `PHASE=send` → run **Phase B** only. - `PHASE=all` → run **Phase C** (A then B). Selector examples: `` → send first list · `contributors-2026-W17` → send that list · `dry-run:contributors-2026-W17` → dry-run send · `plan:` → plan most recent leaderboard · `plan:2026-W17` → plan that week · `plan:dry-run` → plan preview · `all:` → plan + send most recent · `all:dry-run:2026-W17` → full end-to-end preview for that week. --- ## Phase A — Plan (reward computation) Ranks the target week's merged-PR authors (GitHub API) and turns that ranking into a tier-priced list in `memory/distributions.yml`. ### A1. Determine the target week and repo - `REPO="${GITHUB_REPOSITORY:-$(git config --get remote.origin.url | sed -E 's#.*[:/]([^/]+/[^/]+?)(\.git)?$#\1#')}"` — the running instance's repo. - `TARGET_WEEK` comes from the selector; empty = the most recent **completed** ISO week (the last full Mon–Sun). Compute its UTC bounds `WEEK_START`..`WEEK_END` as ISO datetimes (`YYYY-MM-DDT00:00:00Z`). ### A2. Rank contributors by merged PRs in the week Compute the ranking directly from GitHub — no upstream skill or article required. - Fetch every PR **merged inside the window**, by author: `gh api -X GET search/issues -f q="repo:${REPO} is:pr is:merged merged:${WEEK_START}..${WEEK_END}" --paginate --jq '.items[].user.login'` - Drop bot authors (`*[bot]`, `dependabot*`, `github-actions*`). Count each remaining login's merged PRs → `score`. Rank by `score` descending; tie-break by earliest merge time, then login ascending. - **First-PR ✨** per ranked login — did they have any *prior* merged PR to the repo? `gh api -X GET search/issues -f q="repo:${REPO} is:pr is:merged author:${login} merged:<${WEEK_START}" --jq '.total_count'` → `0` means this is their first-ever merged PR (set `first_pr_marker = ✨`). - If zero merged PRs in the window → log `CONTRIBUTOR_REWARD_NO_MERGED_PRS — week ${TARGET_WEEK}` to `memory/logs/${today}.md`, exit silently (no notify). Nothing shipped, nothing to reward. - If the GitHub API is unreachable (see Network note for the `gh api` → WebFetch fallback) → log `CONTRIBUTOR_REWARD_API_FAIL`, notify the operator, exit. ### A3. Load plan idempotency state ```json // memory/state/contributor-reward-state.json { "weeks": { "2026-W17": { "written_at": "2026-04-26T09:00:00Z", "label": "contributors-2026-W17", "source": "github:merged-prs", "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. ### A4. Compute the plan For each ranked login with `rank ≤ 5` AND `score ≥ 1` (at least one merged PR): - 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. ### A5. 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: distribute-tokens "dry-run:contributors-${TARGET_WEEK}" (preview) distribute-tokens "contributors-${TARGET_WEEK}" (execute) ``` If `MODE=dry-run` (plan-only dry-run, i.e. `plan: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. > **`all:` mode note:** when this phase runs as part of `all:` with `MODE=dry-run`, do **not** notify here and do **not** exit — hand the computed plan rows straight to Phase B (see Phase C). When `all:` runs with `MODE=execute`, continue through A6–A8 normally but replace the trailing `Next:` line in the A9 notification with `Distributing now (phase=all)…`. ### A6. Update memory/distributions.yml *(skipped when MODE=dry-run)* Read `memory/distributions.yml`. If missing → bootstrap with the standard header (matching the send-phase bootstrap style): ```yaml # memory/distributions.yml defaults: token: USDC amount: "5" chain: base lists: ``` Compute the new list block: ```yaml contributors-${TARGET_WEEK}:
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub