Skip to content

[design] Charge should be earned by work, not spent by clicking #27

Description

@marsrobertson

The operator's framing

visitors do not expect to charge projects with attention, they charge it with work

That is a product correction, and it happens to close a security hole as a side effect.

What charge does today

/orbit renders a "⚡ charge this" button that POSTs to /api/charge with by hardcoded to "a visitor" (orbitview.py:218-222). The server sums exp(-age_days/7) per project (fleet.py:814-838), so charge decays over a week and a project nobody charges goes dark.

The decay model is good and worth keeping. The input is the problem.

Why the current input is worthless as a signal

A click costs nothing, and the endpoint does not require the click. Anyone can curl it in a loop and pin any project to the top of the orbit view. There is no rate limit today (#17 adds one), and a rate limit does not fix it — throttling a meaningless signal produces a slower meaningless signal.

The footer already states the intent: "energy is a vote of attention, and attention is the only currency here." But attention was being spent by clicking, which is not attention. Work is.

The proposal

Derive charge from contribution rather than accepting it as an assertion:

  • a merged PR against the project
  • an issue filed or a review left
  • an art submission accepted
  • a signal promoted through the airlock (/api/signals/{id}/promote)

Keep the exponential decay exactly as it is — that part is right, and it means a project stays lit only while work keeps arriving.

What this removes

POST /api/charge stops existing. That is the last anonymous write carrying attacker-controlled text into events.jsonl, which is the source half of #16 (stored XSS via the event seed) and the disk-fill vector in #17. The escape merged in #24 stays as defence-in-depth rather than as the only thing standing there.

It also removes a whole class of future bug: charge becomes a computed value, so there is no endpoint to harden, rate limit, or forget to sanitise.

Open questions

  • Where does the contribution signal come from — GitHub webhooks, polling, or the fleet's own event log? Polling is simplest and needs no inbound path.
  • Does an unmerged PR count for anything, or only merged work? Counting attempts is gameable again; counting only merged work makes charge lag reality by review latency.
  • What happens to /orbit's button? Options: remove it, or make it a link to "how to contribute", which turns a dead control into a funnel.
  • Contributions arrive on GitHub, which means charge would reflect only work visible there. Is that acceptable, or does the signature pad / guest book need a path to count too?

Note

/api/charge is currently live and public on both funnelled boards, with zero charges recorded on either. Nothing is lost by changing the input now — there is no history to migrate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions