On this page
Funding model
CommonLayer is funded by contributions that go to projects, not to people. Each project has a treasury with a monthly target and an allocation policy; once a month the treasury pays the maintainer slots whose holders fulfilled their responsibilities, funds shared infrastructure, and keeps a flexible pool for community rewards and a reserve for continuity. This page explains the model as implemented; the precise accounting rules are in the ledger specification.
Funding follows the artifact. Compensation follows the responsibility.
What this page is and is not
- It describes the code in
crates/commonlayer-funding, which is implemented and tested against PostgreSQL (WP-14: 29 unit tests and 58 integration tests, including the nine settlement examples below and a fifteen-step end-to-end scenario; WP-70b added the regression tests of the security review). The acceptance test of workflow B runs the whole composition with the simulated provider's signed webhooks, and the portal's checkout ran end to end in Playwright against the simulator. - It is not a promise that money is flowing. As of this writing no production
deployment exists, the payment provider defaults to
none(contributions are closed), live Stripe payments and Stripe Connect payouts have been tested only against recorded fixtures and a mock server, and real payouts stay disabled until an operator configures a legal entity, confirms tax handling and passes every payout gate. - The figures in the examples are the default policy for a $500 target, used to explain the mechanics. They are not statistics.
Principles
- A project owns its money. Contributions are credited to the project's treasury. No ledger entry references a maintainer until an obligation is created for a specific month and slot. Replacing a maintainer therefore moves no money.
- Compensation is for fulfilled responsibility. It is never paid per vulnerability
and never paid automatically: a maintainer slot is paid only when the maintainer's
eligibility for the month is finalized as
eligibleorat_risk. See the maintainer handbook. - Nothing is redistributed silently. Every movement in a monthly settlement is its own line with a plain-language explanation: obligations created or held, forfeitures, vacancies, sweeps, reserve draws.
- Funds that are not available cannot be spent. The database refuses any posting that would take an internal account below zero.
- Financial records are never edited. Corrections are new, explained transactions.
The treasury
When a project admin configures funding (PUT /api/v1/projects/{slug}/funding/config), the
project gets a treasury in one currency (default USD; no currency conversion) with five
accounts:
| Account | Holds |
|---|---|
available | Contributions not yet allocated |
flexible_pool | Money set aside for community rewards and emergency work |
reserve | The continuity reserve |
obligations | Money owed to named people for a specific month or reward, not yet paid |
paid_out | Cumulative compensation paid (a boundary account) |
The platform has a shared infrastructure_treasury that receives each project's
infrastructure slot and contributions earmarked for infrastructure, and pays recorded
infrastructure expenses.
The public funding page of a project shows its target, coverage, the balance of each
account, the refundable balance (min(available, unrefunded contributions still inside their refund window)), the reserve runway, the active policy, cycle summaries with
every line, and recent contributions.
Allocation policy
A policy is a versioned set of slots that sums exactly to the monthly target (enforced by the database). The default, generated for any target with the largest-remainder method:
| Slot | Kind | Share | For a $500 target |
|---|---|---|---|
primary | maintainer (primary role) | 40 % | $200 |
secondary | maintainer (secondary role) | 30 % | $150 |
infrastructure | infrastructure | 10 % | $50 |
flexible | flexible pool | 20 % | $100 |
A project admin can create other versions (POST /api/v1/projects/{slug}/funding/policies)
and activate one; the active policy is snapshotted when a month's cycle opens. The
infrastructure share must be at least funding.min_infrastructure_bps (default 10 %).
Policy options include: how slots are sized in a shortfall (pro_rata or priority),
where an ineligible maintainer's share goes (reserve or flexible_pool), where a
vacant slot's share goes, daily proration of hand-overs, the reserve target in months
(default 3), whether the reserve may top up a shortfall, and whether an unused flexible
pool is swept to the reserve at the end of the month (default) or carried over.
Contributions
- Anyone can contribute: a signed-in user, an organization (by a member with
org.contributions.create; portal/fund?organization=<slug>), or a guest whose email the payment provider collects. A guest checkout needs anIdempotency-Keyof at least 32 characters and returns a status token that lets the guest follow the payment (GET /api/v1/funding/checkout/statuswithX-Status-Token; portal/fund/complete) without revealing anything about the payer. - One contribution can fund several projects and the infrastructure treasury at once (items), one-time or monthly. A monthly contribution creates a new contribution for every successful renewal.
is_anonymoushides the contributor's name on every public page. Contribution messages are public if provided.- A contribution becomes
confirmedonly when a verified provider event says the money arrived. Returning from the checkout page proves nothing. Confirmation posts the money to the target accounts and assigns a receipt number (CLR-<year>-<6 digits>, gapless per year). - Payment fees are booked separately from the gross amount. By default the fee comes
out of the funded accounts (
payments.fee_bearer = contribution); an operator can make the infrastructure treasury bear fees instead. - Partial payments are booked as received (ADR-0031).
Payment providers
| Provider | Use | State |
|---|---|---|
none (default) | Contributions are closed; every funding write answers payments.not_configured | Implemented |
simulated | Development and demos; refuses to start in production; its checkout sends a correctly signed webhook through the real webhook endpoint | Implemented and used by the tests |
stripe | Cards and SEPA Direct Debit through Checkout Sessions; refunds; fees; payouts through Connect when enabled | Implemented; tested with fixtures built from the API reference and a mock server, not against a live account |
bank_transfer | Invoice-paying organizations: instructions with a unique reference; a platform admin records the receipt | Implemented; one-time only, refunds and payouts outside the platform |
A missing credential never falls back to the simulator.
The monthly cycle
A cycle is one calendar month (UTC) for one project. funding.open_cycles opens the
current month and snapshots the active policy. funding.settle_cycles settles a cycle
once settlement_delay_days (default 3) have passed after the month and every
maintainer evaluation for that month has been finalized. Settlement is computed by a pure
function and posted as one ledger transaction:
- Sweep an unused flexible pool to the reserve (default policy).
- Distributable amount
D = min(available, T). IfD < Tand the policy allows it, draw the difference from the reserve, as far as the reserve goes. This keeps maintainers paid while a temporarily underfunded project's reserve lasts. - Size the slots: full amounts if
D = T, otherwise pro rata (or in priority order). - Split each maintainer slot across the people who held it during the month, by day.
Each part becomes an obligation (
pendingif eligible or at risk;heldif under review or not finalized) or is forfeited to the reserve or the flexible pool (ineligible, temporarily suspended). Days without a maintainer are a vacancy line. - The flexible slot goes to the flexible pool; the infrastructure slot to the infrastructure treasury; a reserve slot to the reserve.
- If money remains and the reserve is below its target, the surplus tops up the reserve;
anything beyond that stays in
availablefor the next month.
Worked examples
All with the default policy and a $500 target (T = 50000 minor units). These are
literal test cases (crates/commonlayer-funding/src/domain/settlement.rs, example_1 to
example_9).
| # | Situation | Result |
|---|---|---|
| 1 | $500 available, both maintainers eligible | primary $200 and secondary $150 owed; $100 to the flexible pool; $50 to infrastructure; nothing left |
| 2 | As 1, secondary ineligible, policy sends forfeitures to the reserve | secondary's $150 goes to the reserve as a forfeited_ineligible line naming the evaluation; no obligation for them |
| 3 | $300 available, no reserve | pro rata: $120 / $90 / $60 flexible / $30 infrastructure; coverage 60 % |
| 4 | $300 available, $500 reserve, draws allowed | $200 drawn from the reserve; full allocation; reserve left $300 |
| 5 | $500 available; primary X for days 1–10 of a 30-day month, then Y | X $66.67, Y $133.33 (both eligible) |
| 6 | $800 available, no reserve, reserve target 3 months | full allocation; the $300 surplus goes to the reserve |
| 7 | Primary slot vacant all month | $200 to the reserve as a vacant line |
| 8 | Secondary under review | $150 moved to obligations as held; paid or released after the decision |
| 9 | Treasury holds $10,000; the primary steps down and a replacement is approved | no ledger transaction; every balance unchanged; the next settlement pays the replacement |
Funding state and continuity
Each project has a public funding state computed from
coverage = (committed monthly recurring + one-time inflow this month + carry-over at month start) × 10000 / target in basis points: surplus from 125 %, fully_funded from
100 %, partially_funded from 50 %, underfunded below that; not_configured without an
active policy; paused when the project admin paused contributions. The thresholds are
runtime settings (funding.surplus_bps, funding.fully_funded_bps,
funding.partially_funded_bps). The reserve runway (reserve / target, in months) is
shown next to it.
Consequences are deliberately slow. A project's lifecycle becomes underfunded only
after two consecutive underfunded settled cycles (runtime setting
funding.underfunded_notice_cycles) and a runway below one month (configuration). Before that maintainers and followers get shortfall notifications, and
commitments continue. The funding state is separate from the security state and the
maintenance state: a well-funded project can have overdue vulnerabilities, and an
unfunded one can be clean.
Refunds and disputes
- A contributor (or a platform admin) can request a refund within the refund window
(default 14 days after confirmation, runtime setting
funding.refund_window_days), up to what the target accounts still hold minus refunds already pending. Money already allocated to maintainers is not refundable through the platform (funding.refund_not_available). A requested refund holds what it will take until the provider confirms or refuses it, so concurrent requests cannot together take more than the accounts hold. The ledger changes when the provider confirms the refund. - Refunds and disputes of subscription payments reach the ledger: Stripe invoice payments are linked to their PaymentIntent and charge (security review HIGH-3; tested against a mock of Stripe, not a live account).
- A dispute (chargeback) is not optional: the provider takes the money. The platform
books it from the project in a fixed order:
available, thenreserve, thenflexible_pool, then the shared infrastructure treasury. Reaching the infrastructure treasury, or an amount no account can cover, is recorded as an administrative alert; the ledger never goes negative. A won dispute returns every step to its source.
Paying people
Obligations are paid by payout runs, which a platform admin plans and a second platform admin approves. A dry run is always available and shows who would be paid, how much, and why each skipped obligation is skipped. Real execution requires every gate (ADR-0032): live mode configured, a provider with payouts, a configured legal entity, confirmed tax handling, a clean reconciliation less than 24 hours old, a second approver, and for each payee a verified payout destination and tax status. If any gate fails, nothing moves and the failed gates are listed. Approving a run needs a person in an interactive session; an API token or a service account cannot approve.
A payout whose provider outcome is ambiguous (a timeout, a 5xx, 409 or 429 answer) is
recorded as unknown, its obligation stays scheduled, and it is retried only with the
same provider idempotency key; an administrator resolves it after checking the provider
(ADR-0041). The destination is fixed when the
run is planned.
No bank details are stored by CommonLayer; maintainers' payout destinations are provider references.
Community rewards
Money for community contributors comes only from the flexible pool. A maintainer proposes
a reward for an approved community contribution; up to funding.reward_single_approval_max_minor
(default $100, a runtime setting) one approval suffices, above it two, one of them from a project or platform
admin. Nobody approves a reward for themselves, the proposer never approves (not even a
single-approval reward), approvers are people (never service accounts), and a contribution
carries at most one open or funded reward
(ADR-0045). Final approval moves the amount from the flexible pool to obligations at once;
an insufficient pool refuses the reward (funding.insufficient_flexible_pool) and takes
nothing from other accounts. Pull requests are never rewarded automatically. See the
contributor handbook.
Reconciliation and transparency
An hourly job checks that every balance equals the sum of its entries, that all balances sum to zero, that each project's obligations account equals its open obligations, that contribution and payout states agree with the ledger, that no provider event is stuck, and, for providers that report balances (Stripe), that internal balances match what the provider holds. A mismatch is recorded, alerts operators and blocks payouts.
Public pages show balances per account kind, policies, cycle lines with explanations, contribution amounts with display names (unless anonymous), the project ledger without private metadata, and the infrastructure treasury's allocations and expenses. What is never public is listed in privacy rules.