Supporter codes: a page on the Mac, and a worker that hands them out - #80
Conversation
scripts/code-admin.py reads the Ko-fi CSV, shows who still needs a code and for how many months, and hands out a ready code or mints one, with the redeem link and a draft message to copy. It listens on 127.0.0.1 with a fresh token each launch, and keeps its ledger in a gitignored supporter-ledger.json that starts from the existing trackers. beta-code.py's minting is split into pieces both share. kofi.md records the pricing McCal set on 22 Sep: a donation is one month, Backer and Builder payments a month each, Coffee no code.
The worker gains an admin side for McCal's Mac: stock and health, the codes it handed out with the Ko-fi transaction behind each one, a refill that takes codes minted on the Mac, a claim for Folio Dev, and a test payment that runs the real rules and puts its code back. Ko-fi's own Send test is recorded and costs no code. The admin page grows a Ko-fi worker section with those checks, a warning when a pool runs low, and the payments the worker missed. Its default pricing follows the tiers: a tip earns a month, Backer and Builder a month each, Coffee nothing. Also: a free port instead of a stack trace when the page is already running, and a double-click launcher for it.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 58cb249408
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| POOLS = '{"tiers":{"*":"beta"},"tipFrom":5,"tipPool":"beta"}' | ||
| # McCal, 22 Sep 2026: a tip or donation of any size is one month; Backer and Builder payments are a month each; | ||
| # Coffee carries no code. m1 is the one-month pool that scripts/code-admin.py mints and uploads. | ||
| POOLS = '{"tiers":{"Backer":"m1","Builder":"m1"},"tipFrom":1,"tipPool":"m1","tipCurrency":"USD","tipRates":{"EUR":1.08,"GBP":1.27,"CAD":0.73,"AUD":0.66,"CHF":1.1}}' |
There was a problem hiding this comment.
Honor any-size tips in currencies worth less than USD
The configured tipFrom: 1 is applied after currency conversion, so a valid 1 CAD or 1 AUD tip becomes 0.73 or 0.66 USD and silently earns nothing despite the stated “any size” rule. Because the currency is successfully priced, this also creates no problems row; use a positive-payment rule that does not impose a one-USD floor.
Useful? React with 👍 / 👎.
| owed = [x for x in person["payments"] if not x["code"] and x["months"]] | ||
| person["owed"] = [x["id"] for x in owed] | ||
| person["months"] = min(MAX_MONTHS, sum(x["months"] for x in owed)) |
There was a problem hiding this comment.
Preserve credits beyond the 15-month code cap
When a supporter has more than 15 uncovered qualifying payments, this caps the generated code at 15 months, but give() later attaches the entire person["owed"] list to that code. The remaining payments are therefore marked covered without receiving their months; only the payments represented by the capped code should be consumed so the rest remain owed.
Useful? React with 👍 / 👎.
| except ValueError: | ||
| for code in codes: | ||
| self.ledger.add(code, note="minted for the worker, which didn't take them") |
There was a problem hiding this comment.
Keep ambiguous refill failures out of the ready pool
If /admin/pool commits successfully but the response is lost or times out, Worker.call() raises ValueError and these same codes are recorded locally as ready to hand out. The worker can then give one of them to a payer while the admin page gives it to someone else; timeout/connection failures must retain a pending/pool state or reconcile by retrying the same idempotent upload rather than making the codes available locally.
Useful? React with 👍 / 👎.
| # ln -s ~/dev/duo-fold-launcher/scripts/"Folio Codes.command" ~/Desktop/ | ||
| # | ||
| # macOS may ask once whether you're sure you want to open it, because it didn't come from the App Store. | ||
| cd "$(dirname "$0")/.." || exit 1 |
There was a problem hiding this comment.
Resolve the checkout before following the launcher symlink
When the launcher is invoked through the documented Desktop symlink, $0 is the Desktop path, so this changes into the user's home directory and python3 scripts/code-admin.py fails. Resolve the symlink target before deriving the checkout directory; a literal copied launcher likewise cannot satisfy the claim that it continues to run this checkout without storing its location.
Useful? React with 👍 / 👎.
Two steps of the supporter code admin plan (rows A1 and A2 of the 0.6.7 build plan). Nothing here is deployed, and the signing key never leaves McCal's Mac: the worker only ever hands out codes minted there in advance.
The page on the Mac (
scripts/code-admin.py)Run it from the checkout that holds
supporter-key.pem, or double-clickscripts/Folio Codes.command. It listens on 127.0.0.1 with a fresh token each launch.folio://redeemlink and a draft message to copy.BetaKeys.WITHDRAWN.supporter-ledger.json(gitignored, 0600, real codes and emails). The first run takes in the codes already insupporter-codes*.txt, so nothing is handed out twice.scripts/beta-code.pykeeps its behaviour; its minting is split intoscope_bits,shape,signanddescribeso both tools share one code format.The worker's admin side (
tools/kofi-worker/admin.js)Everything under
/admin/needsAuthorization: Bearer <ADMIN_TOKEN>and is off until that secret is set.GET /admin/health,GET /admin/recent,POST /admin/pool,POST /admin/test,POST /admin/claim.handledkeeps the Ko-fi transaction id, name, type, amount and currency, so the Mac can match a hand-out to the CSV. No email address is stored.Pricing (McCal, 22 Sep 2026)
A tip or donation of any size earns one month, each payment counting. The tiers stay Coffee $3, Backer $7, Builder $15; each Backer or Builder payment earns a month, and Coffee carries no code. This replaces "$5 = one month" and the $3/$6/$12 idea;
docs/releases/kofi.mdrecords both.Tests
node tools/kofi-worker/admin.test.mjs(new, 7): the admin routes and the webhook against real SQLite with the realschema.sql.node tools/kofi-worker/worker.test.mjs(11) andbeta.test.mjs(9) still pass.BetaCodesTest(9) andBetaCodeToolTest(1) pass against the reworked script.wrangler dev --localwith a throwaway key: refill, health, test payment, a real payment, a Ko-fi test, sync and a missed payment.Still to do
Deploying is McCal's (Cloudflare, the secrets, the Ko-fi webhook address); the runbook is at the top of
tools/kofi-worker/README.md. Folio Dev's "Hand out a code" (row A3) comes next and will use/admin/claim.