Compare commits
257
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
798e8c2e57 | ||
|
|
fd04ea3886 | ||
|
|
89f73a58e4 | ||
|
|
afb5330e08 | ||
|
|
f8c1a1e813 | ||
|
|
3b7bde0568 | ||
|
|
eaac398c0f | ||
|
|
a983b7b6a8 | ||
|
|
9dd06ff181 | ||
|
|
12905d5db1 | ||
|
|
acaf7f0adc | ||
|
|
2ac3f59695 | ||
|
|
d96cec00c7 | ||
|
|
cc343b7d0c | ||
|
|
f43f418ff3 | ||
|
|
ab39dd4a27 | ||
|
|
a90ce03297 | ||
|
|
05c8b21632 | ||
|
|
cf0d45684e | ||
|
|
ce844851ca | ||
|
|
80501e21fd | ||
|
|
36afaac20a | ||
|
|
774ea901e5 | ||
|
|
e5bc9f2ba2 | ||
|
|
f8c6b524ac | ||
|
|
bfca27ac66 | ||
|
|
22de1f885f | ||
|
|
fb0eede6a6 | ||
|
|
c41c88f979 | ||
|
|
ffe6ff2963 | ||
|
|
d87c8d3633 | ||
|
|
7cda37be0d | ||
|
|
9a5648b638 | ||
|
|
7973cb2e0c | ||
|
|
7ef29c8b5a | ||
|
|
45384a0709 | ||
|
|
16d42cc0ef | ||
|
|
6dbb637c3b | ||
|
|
c1570f0680 | ||
|
|
8ec9cc0976 | ||
|
|
2747aae0d1 | ||
|
|
2b31f5c219 | ||
|
|
a196614fb4 | ||
|
|
adf8046773 | ||
|
|
855f9e099d | ||
|
|
139e2b831c | ||
|
|
96d2dbe5ad | ||
|
|
2c269d075d | ||
|
|
7baa6e9810 | ||
|
|
f76b766d0e | ||
|
|
aad548cd07 | ||
|
|
f4271831f4 | ||
|
|
1e82052324 | ||
|
|
a9efaa2725 | ||
|
|
19bba58f7c | ||
|
|
4006764a23 | ||
|
|
a0b16e7d48 | ||
|
|
059299db74 | ||
|
|
20d0ee8910 | ||
|
|
e6befaff5e | ||
|
|
8305c72843 | ||
|
|
1f297c244d | ||
|
|
a77d985135 | ||
|
|
78650c8ee7 | ||
|
|
d4f12c2b47 | ||
|
|
037e29055a | ||
|
|
27dd0ccb3e | ||
|
|
4c1c81fd2f | ||
|
|
7cc964380b | ||
|
|
3e072dbd5d | ||
|
|
4e686aaf72 | ||
|
|
a75c24fcf2 | ||
|
|
56c041f405 | ||
|
|
f9cf2752d2 | ||
|
|
54f951520a | ||
|
|
64852cf6b7 | ||
|
|
0340c33bfb | ||
|
|
b1ed1b9b13 | ||
|
|
e8f930e9fc | ||
|
|
3326557043 | ||
|
|
edf159325b | ||
|
|
6e7b047d3e | ||
|
|
171e878262 | ||
|
|
0f7213ec2d | ||
|
|
853fcedea0 | ||
|
|
295d9eff4a | ||
|
|
fafbd8df32 | ||
|
|
0d878a176d | ||
|
|
7216f3da4d | ||
|
|
e2bb935b43 | ||
|
|
75ec028c3e | ||
|
|
e90bcccada | ||
|
|
14fb68dc04 | ||
|
|
4a1cb842cb | ||
|
|
e7ddff2fed | ||
|
|
f8ed4abc85 | ||
|
|
942919f3f7 | ||
|
|
02c2a034cb | ||
|
|
cd2db6346e | ||
|
|
6eb97c8a35 | ||
|
|
bd8ee11f77 | ||
|
|
13d704301d | ||
|
|
7bd205a91a | ||
|
|
f07144acd8 | ||
|
|
fbc4bd9a3e | ||
|
|
cd57c16050 | ||
|
|
2a080eab6a | ||
|
|
f93e0935ef | ||
|
|
190ccebf6c | ||
|
|
c052ee32c8 | ||
|
|
1f3dda180f | ||
|
|
3ffce9d7c1 | ||
|
|
5d5093f3cc | ||
|
|
aaf96543a8 | ||
|
|
2e9c55af21 | ||
|
|
8b43614b1c | ||
|
|
42b9e83e8f | ||
|
|
419c1de2a0 | ||
|
|
470fbc0d26 | ||
|
|
c4e842f3e3 | ||
|
|
1d9353c94e | ||
|
|
8616636212 | ||
|
|
6174981cdc | ||
|
|
4db6f6e4d3 | ||
|
|
0ad5592687 | ||
|
|
1c2e716508 | ||
|
|
236c52567e | ||
|
|
1689ecb4f0 | ||
|
|
b8655be258 | ||
|
|
55eb569d3b | ||
|
|
7e420ce747 | ||
|
|
86886ac126 | ||
|
|
c36d7f60bd | ||
|
|
f966d8c340 | ||
|
|
15cec18256 | ||
|
|
0e2821257f | ||
|
|
9c8d981e2d | ||
|
|
c19575ab10 | ||
|
|
907e0165db | ||
|
|
8aa90e90a4 | ||
|
|
86e1570618 | ||
|
|
dc5db16ffd | ||
|
|
6df400322e | ||
|
|
b9b2237dbe | ||
|
|
ca9ac7b6af | ||
|
|
fc866597eb | ||
|
|
80dae56f0d | ||
|
|
b97453fba1 | ||
|
|
6ca99fde6b | ||
|
|
419852a1f4 | ||
|
|
8a2fc3e540 | ||
|
|
b64da7a787 | ||
|
|
3be93cdadf | ||
|
|
40fa16920b | ||
|
|
09eac536b5 | ||
|
|
8efc14f61f | ||
|
|
c70cb150db | ||
|
|
e9bc9afcdb | ||
|
|
9f3d0dce4b | ||
|
|
01f5139740 | ||
|
|
c76ebf4d18 | ||
|
|
cc060b29b8 | ||
|
|
3149b48c14 | ||
|
|
cb057b5159 | ||
|
|
ab92259e3e | ||
|
|
65f201cdfd | ||
|
|
d297c1a2c9 | ||
|
|
f0cfd24647 | ||
|
|
95fd46ee99 | ||
|
|
4cc8faf8cb | ||
|
|
cfb83a2ba6 | ||
|
|
4071be5329 | ||
|
|
8a7d7b09bf | ||
|
|
ebf22e3153 | ||
|
|
f0994a8aed | ||
|
|
ddb3907ace | ||
|
|
3fe16fa7f2 | ||
|
|
f199148046 | ||
|
|
38536a5c0c | ||
|
|
286a924fe4 | ||
|
|
3e9e2e899d | ||
|
|
96cd7684f0 | ||
|
|
1af82dfddf | ||
|
|
2cc2c66976 | ||
|
|
a92a389245 | ||
|
|
3d1ce1f45c | ||
|
|
29188d6049 | ||
|
|
4c1ac222a8 | ||
|
|
784315f2ea | ||
|
|
b2262f8604 | ||
|
|
6ad9923aae | ||
|
|
168e8a380c | ||
|
|
edc08d7213 | ||
|
|
0c6cd55951 | ||
|
|
e01d8e7c49 | ||
|
|
c77161c3e9 | ||
|
|
843ef9cd42 | ||
|
|
e5d5b515ba | ||
|
|
a14087fa7d | ||
|
|
76f1952d61 | ||
|
|
f464e6f2e2 | ||
|
|
f5108de288 | ||
|
|
ee37cf7bd9 | ||
|
|
0d1218fa53 | ||
|
|
a72f199139 | ||
|
|
e0eb0d2079 | ||
|
|
d66abda05a | ||
|
|
6fa4532781 | ||
|
|
2f6d63400a | ||
|
|
0d6e7e0b5a | ||
|
|
aeb02d023d | ||
|
|
60d192a9ec | ||
|
|
cc6a1902bc | ||
|
|
a488bbf832 | ||
|
|
9ee4816fa4 | ||
|
|
db208a1a86 | ||
|
|
3d63ba50ee | ||
|
|
073ba9a252 | ||
|
|
c1a484b97a | ||
|
|
6117886e70 | ||
|
|
ae8744672f | ||
|
|
328ef56af5 | ||
|
|
7714e16547 | ||
|
|
cc31cd033b | ||
|
|
f324793732 | ||
|
|
56d9d740b5 | ||
|
|
e5b02b00e4 | ||
|
|
34cd7dbc59 | ||
|
|
4f72dda6fa | ||
|
|
cc1b5011a1 | ||
|
|
18bb23ad6d | ||
|
|
ae591e4212 | ||
|
|
89d79a0f7d | ||
|
|
d0f75d0c14 | ||
|
|
6b3a7dc09d | ||
|
|
38488f2978 | ||
|
|
7c64749267 | ||
|
|
e8d6098d39 | ||
|
|
5d1a6b7399 | ||
|
|
a816b32426 | ||
|
|
5271d5fbad | ||
|
|
552ec1a542 | ||
|
|
5c807d1581 | ||
|
|
a5b9c26902 | ||
|
|
9a5b974fda | ||
|
|
b563a56563 | ||
|
|
72ee2fb2b7 | ||
|
|
ae927983e7 | ||
|
|
4ceeed71a2 | ||
|
|
95c523738c | ||
|
|
1fcc058dd8 | ||
|
|
ee25e2a4fa | ||
|
|
dabf1100c9 | ||
|
|
0759d89d13 | ||
|
|
01a1728971 | ||
|
|
7bedee2934 | ||
|
|
7143e4e701 |
@@ -0,0 +1,26 @@
|
||||
# Unit tests for the Texas Hold'em dealer's money logic.
|
||||
#
|
||||
# The betting engine, the side-pot calculator and the table state are plain script-tag
|
||||
# modules with no build step, so they run directly under Node's built-in test runner —
|
||||
# no package.json, no install step, nothing to keep up to date.
|
||||
|
||||
name: Hold'em tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Run Hold'em unit tests
|
||||
run: node --test tests/holdem/*.test.js
|
||||
+6
-1
@@ -366,4 +366,9 @@ MigrationBackup/
|
||||
FodyWeavers.xsd
|
||||
|
||||
# Native chess engine CMake build output (the compiled .dll/.so are committed under Resources/)
|
||||
native/**/build/
|
||||
native/**/build/
|
||||
# Playwright MCP scratch output
|
||||
.playwright-mcp/
|
||||
|
||||
# Private /socialism working files (founding draft, progress log) - repo is public
|
||||
JoshHeaps.Net/Content/Socialism/_*.md
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
---
|
||||
title: When a Locked Co-op Goes Bankrupt
|
||||
section: The Co-op
|
||||
order: 31
|
||||
summary: Federal bankruptcy law would override the lock, so a failed co-op could be sold whole to anyone, and this page lays out the amendment Congress would need to pass, modeled on how it already treats charities, along with what it costs creditors and how workers get a real chance to buy.
|
||||
---
|
||||
|
||||
# When a Locked Co-op Goes Bankrupt
|
||||
|
||||
[The lock](the-lock.md) says a co-op funded by the bank can never be sold to conventional owners. That holds while the co-op is healthy. It can break at the worst moment, when the co-op fails and a bankruptcy court takes charge of what's left.
|
||||
|
||||
This page explains why, what Congress already does for a similar kind of organization, and what the amendment would say. A few terms first:
|
||||
|
||||
- **The lock-holder** is the independent organization that holds the co-op's *lock share*, a single share that pays nothing but can veto a sale ([The Lock](the-lock.md#how-the-lock-is-made-to-hold)).
|
||||
- **A going concern** is a business sold whole and still running, with its customers, contracts and trained staff. It can be worth much more than the same equipment sold off piece by piece.
|
||||
- **Creditors** are everyone the co-op owes: lenders, suppliers, workers, tax agencies, and anyone it has injured.
|
||||
|
||||
## Why bankruptcy breaks the lock
|
||||
|
||||
When a business files for bankruptcy, everything it owns goes into a pool the court controls, called the *estate*, and is used to pay creditors as much as possible. There are three main routes:
|
||||
|
||||
- **Liquidation (Chapter 7).** A trustee sells what the business owns and pays creditors in order of priority. The trustee can keep the business running for a limited time if that helps sell it ([11 U.S.C. § 721](https://www.law.cornell.edu/uscode/text/11/721)).
|
||||
- **Reorganization (Chapter 11).** The business keeps running under a plan the court approves. Debts are cut or stretched out, and ownership often passes to creditors. Under the *absolute priority rule*, if a group of creditors votes against the plan, the old owners can't keep anything until those creditors are paid in full ([11 U.S.C. § 1129(b)(2)(B)](https://www.law.cornell.edu/uscode/text/11/1129)).
|
||||
- **Small-business reorganization (Subchapter V).** A faster version of Chapter 11 for businesses owing no more than $3,424,000 ([11 U.S.C. §§ 1182(1) and 101(51D)](https://www.law.cornell.edu/uscode/text/11/101), as adjusted in 2025). Here the absolute priority rule doesn't apply. If creditors don't agree to the plan, the owners can still keep the business by paying creditors all of its projected *disposable income*, what's left after the costs of running it, for three to five years ([11 U.S.C. § 1191](https://www.law.cornell.edu/uscode/text/11/1191)). Most locked co-ops would fit under that limit.
|
||||
|
||||
The lock can fail in four places:
|
||||
|
||||
1. **The estate ignores limits on selling.** The Bankruptcy Code says a business's property goes into the estate "notwithstanding any provision" in an agreement or in state law "that restricts or conditions transfer" of it ([11 U.S.C. § 541(c)(1)](https://www.law.cornell.edu/uscode/text/11/541)). The lock is exactly that kind of provision.
|
||||
2. **The court can sell everything, "free and clear."** A trustee can sell the estate's property outside the ordinary course of business, free and clear of other people's interests in it ([11 U.S.C. § 363(b) and (f)](https://www.law.cornell.edu/uscode/text/11/363)). A court could treat the lock-holder's rights as one of those interests and pay them off in cash. In practice, a failed business usually goes to the highest bidder.
|
||||
3. **A reorganization plan can hand the firm over.** A plan can sell the business, merge it or rewrite its charter "notwithstanding any otherwise applicable nonbankruptcy law" ([11 U.S.C. § 1123(a)(5)](https://www.law.cornell.edu/uscode/text/11/1123)). A standard plan cancels the old ownership and issues new shares to creditors. The Code also requires the reorganized company's charter to ban shares that carry no vote ([§ 1123(a)(6)](https://www.law.cornell.edu/uscode/text/11/1123)), while this model relies on outside money that carries no vote ([Capital, After](capital.md)).
|
||||
4. **A lender can sell without going to court at all.** A lender with a claim on all of a business's assets can sell them, or take them, after a default under state law ([UCC § 9-610](https://www.law.cornell.edu/ucc/9/9-610)). No bankruptcy is needed.
|
||||
|
||||
So without a change in the law, a failed co-op's whole business could be bought by a conventional company. The lock would protect only healthy co-ops, which need it least.
|
||||
|
||||
## What Congress already does for charities
|
||||
|
||||
Congress already keeps some organizations' property dedicated to its purpose in bankruptcy. The way it does it is simple: the Bankruptcy Code steps back and lets the ordinary law that governs those organizations decide.
|
||||
|
||||
- **A charity's property can go to a non-charity "only under the same conditions as would apply if the debtor had not filed"** for bankruptcy ([11 U.S.C. § 541(f)](https://www.law.cornell.edu/uscode/text/11/541)). This covers tax-exempt charities.
|
||||
- **Court-approved sales by a nonprofit corporation must follow the law** that governs its transfers outside bankruptcy ([§ 363(d)(1)](https://www.law.cornell.edu/uscode/text/11/363)), and so must transfers under its reorganization plan ([§ 1129(a)(16)](https://www.law.cornell.edu/uscode/text/11/1129)). These three rules were added in 2005.
|
||||
- **Since 1978, creditors can't force a nonprofit into bankruptcy** ([§ 303(a)](https://www.law.cornell.edu/uscode/text/11/303)), and a court can't turn its reorganization into a liquidation unless it asks ([§ 1112(c)](https://www.law.cornell.edu/uscode/text/11/1112)).
|
||||
- **Also since 1978, valid limits on transferring a person's interest in a trust hold in bankruptcy** ([§ 541(c)(2)](https://www.law.cornell.edu/uscode/text/11/541)). That rule keeps some assets away from creditors, so it's a precedent for protecting a purpose at creditors' expense, not a neutral one.
|
||||
|
||||
A locked co-op isn't a charity. It's a business that makes and shares profits. The comparison is narrower: in both cases, assets are meant to stay dedicated to a purpose, not sold for private gain. So the amendment copies the *technique*: the Code defers to the law that governs locked co-ops, and that law, the bank's statute, says what can be sold to whom.
|
||||
|
||||
**Is it constitutional?** Bankruptcy laws must be "uniform." In 1982 the Supreme Court struck down a law written for the workers of one bankrupt railroad, calling it "nothing more than a private bill." But it said a bankruptcy law must "apply uniformly to a defined class of debtors," and that its ruling didn't impair Congress's ability "to define classes of debtors and to structure relief accordingly" ([*Railway Labor Executives' Ass'n v. Gibbons*, 455 U.S. 457](https://www.law.cornell.edu/supremecourt/text/455/457)). A rule for every locked co-op, defined by what it is and not by name, is that kind of class. The parts that reach sales outside bankruptcy (point 9 below) rest on Congress's power over interstate commerce instead.
|
||||
|
||||
## What the amendment does
|
||||
|
||||
### Who it covers
|
||||
|
||||
**1. Every locked co-op on the federal register.** The bank's statute sets up a public register of locked co-ops. A co-op is covered while its lock is on the register. The register decides the question. A notice in the state filing system lenders already search (a UCC financing statement) points to it, but if that notice lapses, the co-op stays locked.
|
||||
|
||||
**2. Old debts are protected.** A lender already secured on the co-op's assets when the lock is registered has to consent in writing before registration. For a business that converts to a co-op, that's part of the buyout deal ([When an Owner Wants to Sell to the Workers](buyouts.md)).
|
||||
|
||||
**3. Creditors who never chose to lend are made whole for the lock's cost.** Some creditors never had a chance to price the lock in: people the co-op injured, tax agencies, and creditors whose unsecured claims came before registration (exactly who counts is below). If the lock makes them recover less than they would have from an open sale, the federation fund pays them the difference. For co-ops outside a federation, the bank pays it. Locked co-ops must also carry standard liability insurance, so injured people are mostly paid by insurers anyway.
|
||||
|
||||
**How big that bill could be.** Counted in cases, usually nothing. Counted in dollars, the cost sits in a few large cases, and nobody has measured how often they happen.
|
||||
|
||||
- **Most failed small businesses have little to fight over.** In one study of small firms liquidated under Chapter 7, creditors got back a median of 5.8% of what they were owed, and an average of 27% (Bris, Welch and Zhu, 2006, as summarized in [Corbae and D'Erasmo, NBER w23515](https://www.nber.org/papers/w23515)). Most small businesses that try to reorganize are liquidated in the end ([Morrison, 2007](https://ideas.repec.org/a/ucp/jlawec/v50y2007p381-419.html)). That's background, not an estimate. Those are firms nobody would buy whole, where the lock changes nothing. The fund pays in the other case: a business still worth more than it owes the bank, brought down by a debt nobody chose, like a large injury claim. That's also where the payment is biggest.
|
||||
- **The bank stands in front.** A failed co-op usually owes the bank more than the business is worth, and any extra an open sale would have brought goes to the bank first. The bank lent knowing about the lock. So the fund only pays when an open sale would have covered the bank and everyone else ranked ahead, with money left over.
|
||||
- **Who's covered.** People the co-op injured; state and local tax agencies; claims listed when the lock was registered, less whatever has since been paid; customers' deposits; and workers who don't join a new co-op, for what they're owed beyond the part bankruptcy law puts first, including damages for late wages and missing layoff notice. Federal tax claims aren't covered, because the federal government chose the lock when it passed the law. Nor are insiders, the parties to a buyout, or anyone tied to them. A court judgment counts only if the fund was told about the case in time and could defend it. Where someone is personally liable for a tax the co-op didn't pay over, like taxes withheld from wages, the fund takes over the claim against them.
|
||||
- **The comparison changes only the price.** Both sides of the comparison use the same list of claims, in the same order, with the same costs of the case, and the insurance pool keeps its place in both. Workers' handed-over claims count as paid in full. The open-market price is taken after the costs a normal sale would have had. Anything a buyer of the pieces pays back under point 8 is subtracted.
|
||||
- **An example.** A co-op owes the bank $100,000, its workers $30,000 of wages the pool paid, and $60,000 in state taxes, and it lost a $400,000 injury lawsuit beyond its insurance. The case costs $40,000. Sold whole on the open market, the business would bring $500,000: after costs, the bank, the wages and the taxes, the injured person would get $270,000. The workers' co-op pays $200,000, so the taxes get $30,000 and the injured person nothing. The fund owes $300,000. A co-op that owed the bank more than the business was worth would owe nothing.
|
||||
- **The open-market figure has to be earned.** Nobody can test it with a real sale, and everyone involved has a reason to bend it: the buyers want the price low, the covered creditors want the gap high, and the payer wants it small. So the court picks the appraiser, and neither the fund nor the bank takes part in choosing. The payer and the covered creditors can each ask for a second appraisal, paid by the fund. The court also invites conventional buyers to make offers. They can't win, but an offer backed by a deposit sets the open-market figure. Otherwise the court decides between the appraisals.
|
||||
- **The buyer pays it back.** The gap is the discount the new co-op got. So the new co-op owes the fund what it paid out, repaid from profit within ten years, behind its bank loan and its debt to the insurance pool. The fund guarantees the creditors are paid now. Only what the new co-op can't repay is a cost the whole sector bears.
|
||||
- **Insurance, checked every year.** The payer, not the co-op, sets how much liability insurance each kind of co-op must carry. It covers claims from the years it was in force even after the co-op fails, and includes pollution cover where the work calls for it. A lapse counts as a default on the co-op's loan and triggers the early warning in [Paying for the workers' bid](#paying-for-the-workers-bid). Co-ops outside a federation pay the bank a premium based on their risk. In a buyout, the selling owner sets aside money for, or guarantees, the debts from before the sale that the co-op takes on.
|
||||
- **During the pilot, the bank pays,** because there's no federation fund yet. Those payments are reported case by case, with each failure's appraised gap, and count as the bank's losses in the losses that can stop its lending ([The Pilot, in Numbers](pilot-spec.md#the-decision-rule)).
|
||||
|
||||
### What can be sold, and to whom
|
||||
|
||||
**4. The Code defers to the lock.** In every chapter, and in plan sales too, the lock's limits hold. The estate takes the co-op's property subject to them. A sale "free and clear" can't strip them. The lock agreements can't be rejected as ordinary contracts. And no plan can override them. The detailed sale rules live in the bank's statute, as the rules for charities live in state law.
|
||||
|
||||
**5. Workers can keep the business running while it's sold.** Argentina changed its bankruptcy law in 2011 to do this. The trustee must report within 20 days on whether the business can keep operating. Workers making up two-thirds of the staff can ask to run it as a co-op while the case goes on, with a plan due within 20 days. The judge decides, and sets a time limit that can be extended once ([Law 26.684, amending articles 190 and 191 of the bankruptcy law](https://www.argentina.gob.ar/normativa/nacional/ley-26684-183856/texto)). The amendment adapts this: workers can ask to run the business as a new co-op, under court supervision, while it's up for sale.
|
||||
|
||||
**6. The whole business goes only to a qualified co-op.**
|
||||
|
||||
- **A qualified bidder** is a locked co-op whose members do at least three-quarters of its work hours, and whose outside money meets the limits in [Capital, After](capital.md): no vote, at most 15 years. A co-op formed by the failed co-op's own workers qualifies from the start. So does the federation. A shell set up by a conventional company doesn't.
|
||||
- **The minimum price is what the assets would bring sold off piece by piece,** set by an appraiser the court appoints. A higher floor, like the full value of a running business, could stop any co-op from buying and force a breakup, which is worse for workers and creditors both. The co-op bidders compete in open bidding, so the price is usually higher than the floor.
|
||||
- **No buying back your own failure cheaply.** People who managed the co-op in the two years before it failed can't get bank financing to buy it ([The Lock](the-lock.md#when-a-co-op-goes-bankrupt)). In a reorganization where the members are running the case and also bidding, an independent officer runs the sale.
|
||||
- **The time limit is 90 days from when the sale is announced, or from when the workers' co-op files its plan, whichever is later.** The court can extend it once if a qualified bidder is lining up financing.
|
||||
|
||||
**7. Workers can pay with what they're owed.** A secured lender can already bid using its claim instead of cash ([§ 363(k)](https://www.law.cornell.edu/uscode/text/11/363)). Argentina lets workers do the same: a workers' co-op can count the wage and severance claims its workers voluntarily hand over toward the price, at their full amount, and the handover happens at a hearing before the judge (Law 26.684, article 27, adding article 203 bis). The amendment does this too, within limits:
|
||||
|
||||
- **Only claims workers choose to hand over,** in writing. Workers who don't join the new co-op keep their claims.
|
||||
- **At full value, up to the amounts bankruptcy law already puts first:** unpaid wages and benefit contributions earned in the last 180 days, up to $17,150 per worker for wages ([11 U.S.C. § 507(a)(4) and (a)(5)](https://www.law.cornell.edu/uscode/text/11/507)). That's a deliberate transfer from other creditors to workers, capped per person, as Argentina chose.
|
||||
- **The legal minimum is paid in cash, not handed over.** Federal law requires minimum wage and overtime to be paid in cash or a cheque ([29 CFR 531.27](https://www.law.cornell.edu/cfr/text/29/531.27)), "finally and unconditionally" ([29 CFR 531.35](https://www.law.cornell.edu/cfr/text/29/531.35)), and workers can't waive them by private agreement ([*Brooklyn Savings Bank v. O'Neil*, 1945](https://www.law.cornell.edu/supremecourt/text/324/697)). A stake in a new company isn't cash. Wage claims are bought and sold in bankruptcy, and handing one over at its full value isn't a discount, so this might be allowed. But the usual way a court blesses giving up wage claims is by approving a settlement of a real dispute over them ([*Lynn's Food Stores v. United States*, 11th Cir. 1982](https://law.justia.com/cases/federal/appellate-courts/F2/679/1350/13398/)), and there isn't one here. So the model doesn't rely on it.
|
||||
- **The protected part** is every unpaid hour at the highest minimum wage that applies, federal, state or local, plus all unpaid overtime at the worker's own overtime rate, taken from payroll records. For salaried staff whose hours weren't recorded, it's counted at 40 hours a week. Any damages the law adds for late wages stay with the worker.
|
||||
- **Some states protect more.** California bars employers from requiring a release of any wages due until they're paid, not just the minimum ([Cal. Labor Code § 206.5](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=LAB§ionNum=206.5)), and no private agreement can get around its wage rules ([§ 219](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=LAB§ionNum=219)). The bank's checked entry for each state ([Owners Are Still Workers](labor-law.md#is-the-profit-share-pay)) says which wages can be handed over there. Where it's unclear, only severance and benefit contributions can be.
|
||||
- **The pool pays first.** The [savings insurance pool](savings-guarantee.md) pays up to eight weeks of each worker's unpaid wages in full, as it always does, and what it pays counts against the protected part first. Any protected wages beyond that stay the worker's own claim, and the court can't approve a bid that uses handed-over claims until the buyer has set aside cash to pay them.
|
||||
- **Workers can hand over the rest:** wages beyond what the pool paid that the protected part doesn't cover, benefit contributions and severance, within the cap above.
|
||||
- **Handed-over claims buy savings, not votes.** In the new co-op, they become credits in each worker's [capital account](capital-accounts.md). Votes stay one per member, so a manager with a larger claim doesn't get more say than an aide whose wages were paid by the pool.
|
||||
- **The insurance pool's claims count too.** Normally, whoever pays a wage claim loses its priority ([§ 507(d)](https://www.law.cornell.edu/uscode/text/11/507)). Without a fix, most low-paid workers' claims would be paid by the pool and couldn't be used to bid. So the pool keeps the priority, and when a qualified workers' co-op bids, it hands the claims it paid to that co-op for its bid. The new co-op repays the pool over time. Each worker's own claims use the $17,150 priority first, and the pool's claim for that worker only ranks after them.
|
||||
- **No last-minute claims.** In the 180 days before filing, pay deferred by a vote, pay left unpaid for more than one normal pay period while others were being paid, and any increase in notice or severance terms rank as ordinary claims, not priority ones, and can't be used to bid.
|
||||
|
||||
**8. If no qualified co-op buys it, the pieces go to anyone, but not the whole.** No buyer, together with anyone financially tied to it, may end up with substantially all of the business. The brand, the core technology or the main customer contracts count as substantially all, whatever their size, as the lock already says. A buyer of the pieces who hires more than a third of the members within two years owes the difference between what it paid and what the running business was worth, as set by the court's appraiser. The sale order says so up front. That money goes to the co-op's creditors, not the federation fund.
|
||||
|
||||
**9. Lenders can't sell around it outside court either.** The same rules apply to any transfer of substantially all of a locked co-op's assets because it can't pay its debts: a lender's sale or taking of its collateral, a state receivership, an assignment for the benefit of creditors, or a tax sale. The [model state law](lock-state-law.md#what-the-model-law-does) writes these rules into state law too, and adds that a sale following these rules counts as "commercially reasonable" under the UCC, so lenders aren't penalized for following them.
|
||||
|
||||
### Paying for the workers' bid
|
||||
|
||||
A workers' co-op can only use these rules if someone lends it the money in time, on terms a failed business can meet. The best-documented US case is New Era Windows in Chicago. Its workers had occupied their factory in 2008. When a later owner closed it in 2012, a group of them formed a co-op. They didn't buy a running business. The plant was shut, and they bought the equipment they wanted at the creditors' auction for $430,000, at salvage prices. They needed $850,000 in all, counting money to restart and run it, and put in about $20,000 themselves, $1,000 each. One mission lender, The Working World, provided the rest, in ten-year loans at 5% repaid only once the co-op made a profit ([Project Equity](https://project-equity.org/ownership-story/new-era-windows-deal-structure/)). The co-op opened in May 2013 ([The Working World](https://www.theworkingworld.org/us/new-era-windows/)). Money repaid only from profit is closer to ownership than to a loan, and a bank lending on past cash flow would probably have turned New Era down. So the case shows what kind of money a restart needs, and the rules below don't pretend the bank's ordinary test can supply all of it.
|
||||
|
||||
- **Warning early, and privately.** Members and the lock-holder are told what these rules let them do at the first of: 60 days behind on a loan from the bank, a missed payroll, unpaid payroll taxes, deferred insurance pool premiums, loan terms eased to avoid a default, lapsed liability insurance, or a layoff notice. Members own the business, so they're told privately, through an adviser, not in a letter that reaches suppliers. Every worker, including temporary and agency staff, is told once a loan is called in or a case is filed, in the languages they speak.
|
||||
- **An adviser who works for the workers.** The workers pick one from a panel the [federation](federation.md) keeps. The adviser is paid from a small charge on all the bank's loans, not by the co-op's managers or by the bank case by case.
|
||||
- **Keeping it running.** A court can already approve new lending to a business in bankruptcy that ranks ahead of the case's other costs ([11 U.S.C. § 364(c)](https://www.law.cornell.edu/uscode/text/11/364)). It can't rank ahead of an existing lender's lien without that lender's protection being shown ([§ 364(d)](https://www.law.cornell.edu/uscode/text/11/364)). When workers run the business while it's up for sale (point 5), the bank may make such loans for day-to-day costs, on limits the statute sets: they can't rank ahead of wages earned while the workers run it, can't be used to pay off the bank's old loans or to pledge assets for them, and can't carry a deadline that forces a sale before the 90 days in point 6. If the workers' co-op wins, the loan can be folded into its purchase loan.
|
||||
- **A separate test for buying a failed co-op.** The bank's usual buyout test looks back at two or three years of checked cash flow ([When an Owner Wants to Sell to the Workers](buyouts.md#the-seller-shares-the-risk)), which a failed firm doesn't have. So the bank lends against what the assets would bring sold off piece by piece, as set by the court's appraiser, and no more. The rest of the price and the money to run it have to come from the workers' handed-over claims, the insurance pool's claims, another lender, or the federation's fund, which can lend on terms like New Era's, repaid only from profit, within the limits it sets in advance ([Investing for the Long Run](the-long-run.md#spending-it-badly)). That also stops the bank from raising the price, and its own recovery, with its own money.
|
||||
- **The same offer to every qualified bidder,** in writing. If the bank turns a bid down, it gives its reasons to the workers and the court. When two qualified bids are equal, the one whose members include more of the failed co-op's workers wins. People who managed the failed co-op in its last two years can join as members, but the bank won't lend to a bid they lead.
|
||||
- **Counted in the bank's record.** Nobody who worked on the failed co-op's loans decides on the new one. The failed co-op counts as closed ([Keeping zombies alive](pilot-safeguards.md#keeping-zombies-alive)). The failed loan's recovery counts only money that didn't come from the bank, and the new loan is reported together with it, as one lending decision. Any loss on the new loan in its first five years is added to the failed loan's loss, in the published recovery rate and in the losses that can stop the bank's lending ([The Pilot, in Numbers](pilot-spec.md#the-decision-rule)). So lending the workers the money to buy a failed co-op can't hide a loss.
|
||||
- **Repaying the insurance pool.** The pool chooses whether to hand over the wage claims it paid (point 7), under a test it publishes in advance. If it does, the new co-op owes it that amount at the one-year Treasury rate plus two points, the rate members' accounts earn, secured by a lien behind the bank's. That's below what a lender would charge a restarted business, a subsidy from the other co-ops' premiums, and it's stated as one. The co-op repays within ten years, which the pool can extend once. Each year's payment comes before any pay above the going rate and before any profit share paid in cash beyond what members need for tax.
|
||||
|
||||
### When the co-op reorganizes
|
||||
|
||||
**10. A reorganized co-op stays a co-op, and creditors are treated as they would be anyway.**
|
||||
|
||||
- **Only working members hold votes.** A plan can give creditors new debt, or voteless claims on the terms in [Capital, After](capital.md), but never ownership, and no loan terms giving a lender approval over the co-op's management or strategy. The rule requiring a ban on voteless shares doesn't apply.
|
||||
- **Small co-ops follow the ordinary small-business rule.** In Subchapter V, owners of any small business can keep it by paying creditors all of its disposable income for three to five years. A co-op gets the same deal, no better. To stop members shrinking that income by paying themselves more, disposable income is worked out with members' pay at the going rate ([Investing for the Long Run](the-long-run.md#the-rules)), and raises in the two years before filing are reversed.
|
||||
- **Larger co-ops pay creditors the business's full value.** If a group of creditors votes against the plan, they must get at least what the business would sell for on the open market, as if the lock didn't exist, in cash, new debt or voteless claims. That's what the absolute priority rule protects in any company. Members can keep a co-op they can't sell, but not at their creditors' expense.
|
||||
- **That value is tested, not just appraised.** The Supreme Court held in 1999 that owners can't keep a business over a creditor group's objection through a deal nobody else could compete for, because "the best way to determine value is exposure to a market" ([*Bank of America v. 203 North LaSalle Street Partnership*](https://www.law.cornell.edu/supremecourt/text/526/434)). It left open what kind of market test is enough. Here the test is weaker than a real one, because a conventional buyer can never win. So when members propose to keep a co-op over a creditor group's objection, the court picks the appraiser, as in point 3, and invites offers for the business. Offers have to be in binding form, with a deposit the offeror loses if the offer turns out to be unfinanced or is withdrawn, and creditors and anyone tied to them can't make them. The fund pays reasonable costs of checking out the business to offerors who make one. The highest such offer sets the floor. If none comes in, the floor is the higher of the two appraisals. Any creditor can also propose a competing plan, including a sale to another qualified co-op, and the members lose their exclusive right to propose a plan. Whether that satisfies the courts is untested. Small co-ops under Subchapter V aren't affected: the absolute priority rule doesn't apply there, and only the debtor can propose a plan ([§ 1181(a)](https://www.law.cornell.edu/uscode/text/11/1181)). That's also a way around the test, so a co-op facing injury claims above its yearly sales can't use Subchapter V.
|
||||
- **Members' own accounts go first.** What members have in their capital accounts is wiped out before creditors lose anything. Members were taxed each year on those allocations, so this is a real loss, and it's the cost of being an owner ([Member Capital Accounts](capital-accounts.md#protecting-the-co-op-and-the-people-who-stay)).
|
||||
|
||||
### Everything else
|
||||
|
||||
**11. Former members are paid before current members.** A former member's payout is a fixed debt the co-op owes ([Member Capital Accounts](capital-accounts.md#how-its-paid-out)). It ranks behind every other unsecured creditor, but ahead of current members' accounts. It isn't pushed down to the bottom as if it were a shareholder's claim ([§ 510(b)](https://www.law.cornell.edu/uscode/text/11/510)). Anyone who left within a year before the failure has their balance recalculated first, as the insurance pool already does ([Protecting Members' Savings](savings-guarantee.md#closing-the-loopholes)).
|
||||
|
||||
**12. The lock-holder can go to court, carefully.** The lock-holder can take part in the case. If the trustee declines to challenge a deal the co-op made with insiders before it failed, the lock-holder can ask the court's permission to challenge it, and pays the other side's costs if the court finds the challenge frivolous.
|
||||
|
||||
**13. Leased equipment, licenses and sold invoices.** A co-op doesn't own what it leases or licenses, or the unpaid invoices it has sold to a lender (called factoring), so the lock can't hold them directly. A business that rents its shop, leases its machines and licenses its software could be taken apart by the people it rents from. So the lock sets terms for the things a co-op can't run without.
|
||||
|
||||
- **In bankruptcy, many leases can go with the business.** The trustee can keep a lease and hand it to the buyer, even if the lease forbids transfers, as long as rent that's behind and the lessor's other money losses are made good and the buyer shows it can pay ([§ 365(b) and (f)](https://www.law.cornell.edu/uscode/text/11/365)). There are big exceptions. A commercial lease the landlord ended before the filing can't be revived. Where other law lets the other side refuse to deal with anyone new, as it usually does for patent, software, trademark and franchise licenses, the license can't be handed on without consent, and in several circuits the co-op can't even keep it (§ 365(c)(1)). Credit lines, including a factoring line, end at filing (§ 365(c)(2)).
|
||||
- **Deadlines are held open, and the user pays the rent.** In a liquidation, leases of equipment and other ongoing contracts count as ended if they aren't kept within 60 days (§ 365(d)(1)). A commercial property lease ends after 120 days, extendable once by 90, and after that only with the landlord's written consent (§ 365(d)(4)). The co-op sale can outlast those deadlines. So the amendment holds them open until the co-op sale ends, which has its own time limit (point 6). The rent in the meantime is paid by the workers' co-op running the business under point 5 if there is one, and otherwise by the federation fund, or the bank in the pilot, not by the other creditors. The lessor can still ask the court to protect its property from damage.
|
||||
- **Essential leases and licenses carry the lock's terms from the start.** The things a co-op can't run without are its premises, its main production equipment, the software that holds its customer and operating records, and its brand or franchise. Any lease or license of one of those has to:
|
||||
- consent in advance to being handed on to a locked co-op that takes over the business, including the workers' own new co-op under point 5, on the same terms and with no transfer fee or rent increase. The lessor must accept that co-op if the federation fund, or the bank in the pilot, guarantees its first 12 months of payments. Advance written consent is what the bankruptcy exception for licenses requires;
|
||||
- not let the lessor end it just because the co-op is insolvent or in bankruptcy, only for real default after notice and a chance to fix it;
|
||||
- for premises, give a co-op successor the same renewal rights the co-op had; for software, let the co-op export its own data in a usable form.
|
||||
|
||||
The lock-holder checks these clauses when the lease is signed. Where the only supplier of something is a large firm that won't negotiate, like a mass-market software service, the lock-holder can waive the clauses if the co-op can move its data and switch to a comparable supplier. Some lessors will charge more for these terms, or refuse to deal.
|
||||
- **A sale-leaseback is a sale.** Selling something the co-op owns and leasing it back counts toward the lock's limit on selling a quarter of the business in three years ([The Lock](the-lock.md#closing-the-back-doors)), and it's borrowing for [the second-opinion rule](the-long-run.md#spending-it-badly). Selling and leasing back the premises or main equipment needs the lock-holder's consent, whatever its size, and the lease has to carry the terms above.
|
||||
- **Nobody can reassemble the business through the back door.** Point 8 already charges a buyer of the pieces who hires more than a third of the members within two years. The same goes for anyone who, within two years, gets the use of property the co-op leased, licensed or financed, by buying, leasing or licensing it, and runs the same business with more than a third of the co-op's former workforce, members or not, or takes over its main customer contracts. A lessor that re-leases to a new company is caught through that company. What's owed is the value of the running business, less the fair value of the property the payer already owned or paid for. In bankruptcy it goes to the creditors, as in point 8. Outside bankruptcy it goes to the federation fund, as the lock's matching rule says.
|
||||
- **A "lease" that's really a loan is the co-op's property.** If the co-op can't end the lease early and has to pay for the equipment's whole useful life, or can own it at the end for a token price, the law treats the deal as a loan secured on the equipment ([UCC § 1-203](https://www.law.cornell.edu/ucc/1/1-203)). Then the equipment is the co-op's, still subject to the lender's claim, and a lender's sale of it follows point 9. Lessors can easily write leases that stay on the right side of that test, so this catches only careless ones.
|
||||
- **Selling invoices is limited, not banned.** Selling invoices one at a time is a common way for small firms to get paid sooner, and it doesn't move the customers, workers or equipment. Some deals go further. A merchant cash advance buys a share of future sales, takes daily withdrawals from the bank account, and often comes with a blanket claim on everything the firm owns and a signed confession of judgment. So, as part of the lock, for as long as it lasts:
|
||||
- buying a share of future sales counts as outside money, on the terms in [Capital, After](capital.md), or as a loan;
|
||||
- a buyer of invoices can have a claim only on the invoices it bought, never on everything the co-op owns;
|
||||
- no confessions of judgment;
|
||||
- no clause barring the co-op's customers from dealing with a successor co-op.
|
||||
|
||||
Selling invoices counts as borrowing for the second-opinion rule, and it's reported to members each year alongside new leases. While the co-op owes the bank, it needs the bank's consent, which is given automatically while the invoices sold and not yet collected are under a tenth of a year's sales.
|
||||
- **It's measured.** The pilot publishes how much of its co-ops' premises, equipment and software is leased or licensed, what the rent costs as a share of what the co-op produces, and how concentrated the lessors are, so if this becomes a way around the lock, it shows.
|
||||
|
||||
**14. Customers and people hurt later.** A buyer of a bankrupt business usually takes it "free and clear" of the old company's debts. That's the ordinary rule, not the lock's. But a workers' co-op that buys a failed co-op is often the same people, making the same things under the same name. Outside bankruptcy, state law can make a buyer answer for the seller's debts when it's a "mere continuation" of the seller, and a few states, like California, make a buyer that carries on the same product line answer for injuries from the seller's products ([*Ray v. Alad Corp.*, 1977](https://static.case.law/cal-3d/19/cases/0022-01.json)). How far a bankruptcy sale cuts those claims off is unsettled for people who are hurt only after the sale. When GM was sold in 2009, the appeals court later held that the sale order couldn't bind people who bought used Old GM cars after the sale, since they'd had no dealings with Old GM ([*In re Motors Liquidation Co.*, 2d Cir. 2016](https://cases.justia.com/federal/appellate-courts/ca2/15-2844/15-2844-2016-07-13.pdf)). Points 3 and 6 already stop members from using a failure to shed claims people have now: the fund pays the covered creditors what the lock cost them, and the new co-op pays the fund back. This point covers the people who aren't in the case yet.
|
||||
|
||||
- **Whoever carries on the business takes on a minimum.** GM's buyer agreed to answer for accidents after the sale, to make repairs under the old company's written warranties, and to answer for lemon-law claims. That was a negotiated deal, under political pressure, and most buyers in bankruptcy sales don't take on injury claims. The lock requires it. These obligations bind a co-op that buys the whole business under point 6, a co-op whose members keep it through a plan under point 10 or Subchapter V, and anyone caught by the back-door rules in points 8 and 13, in court or out of it:
|
||||
- injuries after the sale caused by products the old co-op made or work it did. In a plan, these aren't wiped out by the discharge;
|
||||
- repairs or replacements under the old co-op's written warranties. Money claims for breaches before the sale stay in the case;
|
||||
- household customers' deposits for things not yet delivered: the business delivers, or refunds the deposit, up to the amount bankruptcy law already puts ahead of other unsecured claims per person ([§ 507(a)(7)](https://www.law.cornell.edu/uscode/text/11/507)). A delivery or refund cancels that customer's claim in the case and any payment for it under point 3, so nobody is paid twice. Business customers' deposits stay in the case.
|
||||
|
||||
These are a minimum. The amendment doesn't cut off any claim against a buyer that state law would allow.
|
||||
- **Who pays for it.** In a sale, these come on top of the cash price, which still has to meet the floor in point 6, so the bidders price them in. In point 3's comparison, the open-market sale carries only what the appraiser finds buyers usually take on in such sales, like warranty repairs and deposits, not future injuries. So protecting people hurt later doesn't shrink what the fund pays people hurt already. The bank's bid lending counts the estimated cost too.
|
||||
- **Insurance carries most of it.** Injuries from the old co-op's products and completed work have to be insured, on the terms the payer sets for all co-ops (point 3). The estate can buy that cover, called tail or discontinued-products insurance, before creditors are paid, or the buyer can. Where nobody sells it at a sane price, the buyer sets aside a reserve from its profit instead, sized by the representative below and backed by the federation fund.
|
||||
- **The limits.** Beyond insurance, what the lock requires lasts ten years after the sale or the state's own limit on product suits, whichever is longer. It's capped, in total for all such claims, at the business's open-market value found under point 3. There's no cap or time limit where insiders hid a defect they knew about, or where a known claimant didn't get notice. If the new co-op is later sold, the next buyer takes on what's left.
|
||||
- **Someone speaks for people who aren't hurt yet.** Where the old co-op built, made or handled something that could injure people years later, like buildings, machinery, food or chemicals, the court appoints a representative for future claimants. The idea is borrowed from asbestos cases ([§ 524(g)(4)(B)(i)](https://www.law.cornell.edu/uscode/text/11/524)), where the representative's job is bargaining over a trust. Here it's narrower: the representative can object to the sale, the plan, the insurance, the reserve and the value. Known claimants get notice by mail, not just by publication. The GM ruling held that people the old company knew or should have known about, and who didn't get real notice, weren't bound by the sale.
|
||||
- **Outside bankruptcy, state law still applies.** When a lender sells a failed co-op under point 9, there's no court order cutting off old debts, and state successor-liability law applies in full, on top of the minimum above. It's one more reason a workers' bid will usually go through the court.
|
||||
|
||||
**What it doesn't do.** Unlike charities, locked co-ops *can* be forced into bankruptcy by their creditors, and a failed reorganization *can* be turned into a liquidation. A business that can't pay its debts shouldn't be able to shelter behind the lock. The amendment only decides who can buy it, not whether it can fail.
|
||||
|
||||
## What it costs, and who pays
|
||||
|
||||
- **Lenders get back less when a co-op fails.** The whole business can only go to a co-op, so it may sell for less than on the open market. Lenders who lend after the lock is registered know that, and they'll charge for it, in higher rates or more collateral.
|
||||
- **The bank bears it first.** The bank is usually the biggest lender, so the pilot publishes its recovery rate on failed loans ([The Pilot, in Numbers](pilot-spec.md#what-gets-measured)). That shows what the lock costs.
|
||||
- **The federation fund bears the rest.** Creditors who never chose to lend are paid the lock's cost from the fund. Most failures cost it nothing, but a single case with a large uninsured claim could cost it hundreds of thousands of dollars or more, until the co-op that bought the business pays it back. That's a real call on money the co-ops put in.
|
||||
- **Other lenders may lend less, or not at all.** Some banks will refuse locked co-ops, or lend only against equipment they can take back. Lenders willing to fund a failing co-op through a rescue may be harder to find, because they can't sell the business to anyone. That's a real cost, and it's part of why the model needs its own bank.
|
||||
- **Workers' claims come ahead of other creditors when workers bid.** That's a choice to favor keeping a business running in its workers' hands. It's capped per worker, and it's paid for by other creditors.
|
||||
- **Low-paid workers bring less of their own to the bid.** Their unpaid wages are mostly the legal minimum, which is paid in cash rather than handed over, so more of the bid rests on the pool's claims and on better-paid workers' claims. Whether the rule is needed at all depends on a legal question nobody has tested: whether a worker can freely hand over a wage claim at full value.
|
||||
- **Some viable businesses will still be broken up.** When no co-op can buy a failed co-op whole, a conventional buyer who would have kept it running and kept the jobs can't. While the co-op sector is small, this will happen. Whether to allow a conventional buyer as a last resort, with any surplus going to the federation fund, is an open question ([Open Problems](open-problems.md)).
|
||||
|
||||
## Draft text
|
||||
|
||||
For readers who want to see what this could look like in the statute, here is a first draft of the core Bankruptcy Code changes. The detailed sale rules (points 5 to 9) go in the bank's own statute. This is a starting point for lawyers, not finished legislation.
|
||||
|
||||
> **Sec. 101 (new definition).** The term "locked cooperative" means an entity entered, as of the date of the filing of the petition, on the register of locked cooperatives kept under [the bank's authorizing act].
|
||||
>
|
||||
> **Sec. 541(g).** Notwithstanding subsection (c)(1) and any other provision of this title, property held by a debtor that is a locked cooperative may be transferred only as permitted by the nonbankruptcy law governing locked cooperatives, as if the debtor had not filed a case under this title.
|
||||
>
|
||||
> **Sec. 363(q).** In a case in which the debtor is a locked cooperative, (1) the trustee may use, sell, or lease property under subsection (b) or (c), including under subsection (f), only in accordance with the nonbankruptcy law governing locked cooperatives; (2) the rights of a lock-holder under that law are not an interest that may be sold free and clear under subsection (f); and (3) section 365 does not permit rejection of any agreement required by that law.
|
||||
>
|
||||
> **Sec. 365(p) (new).** In a case in which the debtor is a locked cooperative, (1) no period under subsection (d)(1) or (d)(4) shall end before the close of any sale of the debtor's business under the nonbankruptcy law governing locked cooperatives, and the obligations of the debtor under such contract or lease arising during the extended period shall be paid as provided in that law and shall not be an administrative expense of the estate; and (2) consent given in a contract or lease to assignment to a locked cooperative that acquires the debtor's business is consent for purposes of subsection (c)(1)(B).
|
||||
>
|
||||
> **Sec. 507(d) (amended).** Insert at the end: "except that an insurance pool established under [the bank's authorizing act] that pays a claim of a kind specified in subsection (a)(4) or (a)(5) is subrogated to the holder's right to priority, but such priority is subordinate to any claim of the same individual under subsection (a)(4) or (a)(5) that the pool has not paid."
|
||||
>
|
||||
> **Sec. 1123(e).** Notwithstanding subsection (a), a plan for a debtor that is a locked cooperative (1) may not provide for any transfer of property, merger, consolidation, or amendment of the debtor's charter except as permitted by the nonbankruptcy law governing locked cooperatives; and (2) need not comply with subsection (a)(6) as to any nonvoting interest permitted by that law.
|
||||
>
|
||||
> **Sec. 1129(a)(17).** If the debtor is a locked cooperative, (A) all transfers of property under the plan are made in accordance with the nonbankruptcy law governing locked cooperatives; and (B) no equity security carrying voting power is issued to any entity other than members who work for the reorganized debtor.
|
||||
>
|
||||
> **Sec. 1129(b)(2)(B)(iii).** In a case in which the debtor is a locked cooperative, membership in the reorganized debtor held by persons who work for it is not property received or retained on account of a junior interest, if each holder of a claim of the class receives or retains under the plan property of a value, as of the effective date of the plan, not less than the amount such holder would receive if all of the debtor's property were sold as a going concern without regard to section 541(g), as determined under the nonbankruptcy law governing locked cooperatives after the court has invited binding offers for the debtor's business and has permitted any party in interest to file a competing plan, and not less than the highest such offer.
|
||||
>
|
||||
> **Sec. 1121(f) (new).** In a case in which the debtor is a locked cooperative, if the debtor files a plan under which members retain membership over the objection of an impaired class, any party in interest may file a plan.
|
||||
>
|
||||
> **Sec. 1182(1) (amended).** Insert at the end: "A locked cooperative is not a debtor under this subchapter if unliquidated claims for personal injury or wrongful death against it exceed its gross receipts for the preceding year."
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **It needs Congress, and a later Congress can undo it.** The lock's other layers hold if the bank's law is repealed. This one doesn't ([Making It Last](durability.md)).
|
||||
- **Courts will test it.** Treating membership as something other than property the owners keep is new in bankruptcy law. The draft ties it to paying dissenting creditors full open-market value, which answers the fairness point, but the first cases will still be fought.
|
||||
- **Reaching lenders' sales outside court is a federal reach into state law.** It rests on Congress's power over commerce, not bankruptcy, and it will be challenged. States that adopt the [model law](lock-state-law.md#what-the-model-law-does) make it much safer.
|
||||
- **Renting leaves the co-op exposed.** Essential leases and licenses have to allow a co-op successor (point 13), but a landlord can still refuse to renew when a lease runs out, and raise the rent to whatever the market will bear. A co-op that rents most of what it uses is paying a lot of its surplus to owners outside it, and nothing in the lock stops that. Only buying its premises and equipment does. The terms above will also make some lessors charge co-ops more, or refuse them, and mass-market software can't be made to follow them at all.
|
||||
- **The money only comes if the numbers work.** Many businesses fail because they can't pay their way, and a lower price doesn't always fix that. The bank lends only up to what the assets would bring sold off, so the rest has to come from patient money. In the pilot's early years there's no federation fund to provide it, and where nobody does, the business is sold in pieces. And the bank, usually the biggest lender to the failed co-op, also decides whether to lend to its workers' bid. The rules above keep those decisions apart. They can't make the bank neutral about its own losses.
|
||||
- **The register has to exist and be trusted.** It's a new federal job, and every lender to a co-op will have to check it.
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: Help Before the First Loan
|
||||
section: Growing the Sector
|
||||
order: 67
|
||||
summary: The pilot's hardest job is getting enough good co-ops started, so it tests, by lottery, whether serious help before launch creates more of them that last, and asks founders for a small stake they can pay in work instead of cash.
|
||||
---
|
||||
|
||||
# Help Before the First Loan
|
||||
|
||||
The pilot aims to fund about 400 new co-ops in five years, about 80 a year. The whole American worker co-op sector grew by about 50 a year over the last decade, after counting the co-ops that closed ([The Pilot, in Numbers](pilot-spec.md#why-about-400-co-ops)). If good groups don't show up, the bank has nobody to lend to, and the whole test stalls.
|
||||
|
||||
The pilot already tests help *after* the first loan, by lottery ([the support lottery](pilot-spec.md#the-support-lottery)). This page is about help *before* it: whether serious support for groups who want to start a co-op makes more of them succeed in starting one that lasts, and whether founders should put in something of their own.
|
||||
|
||||
## What we know
|
||||
|
||||
**Mondragon's help started before a co-op existed.** When a group came with an idea, its bank picked one of them to be the manager, paid that person a salary while a team studied whether the business could work, and later lent the new co-op enough to cover its losses for its first two years ([Has This Been Tried?](precedents.md#mondragons-bank-caja-laboral)). Nobody knows how much of Mondragon's success came from that help and how much from the loans.
|
||||
|
||||
**Light training doesn't last.** At the time, the largest randomized trial of entrepreneurship training ever run was America's GATE project. It assigned 4,197 people who wanted to start businesses to be offered free training or not. People not offered it could still find training elsewhere, so the real difference was modest: about 9 more hours of classes and advice, at about $1,321 per person. Six months later, the trained group was about 5 percentage points more likely to own a business. By 18 months the gap in who currently owned a business was small and no longer clear, and by five years there was no clear difference ([Fairlie, Karlan and Zinman, 2012](https://www.nber.org/papers/w17804)).
|
||||
|
||||
**Heavier help is expensive, and nobody has measured what it does.** New York City has funded community groups that help people start worker co-ops since 2015. Its partners often work with a group for several years. In the program's seventh year, a pandemic year, it cost about $3.1 million, and 11 new co-ops formally launched or converted, meaning they got a tax ID, formation papers and permits. Another 21 were in progress ([NYC Small Business Services, 2021](https://www.nyc.gov/assets/sbs/downloads/pdf/about/reports/worker_coop_report_fy21.pdf)). That's about $280,000 for each co-op launched that year, though the program also helps existing co-ops, so it isn't a true cost per co-op. And there's no comparison group, so nobody can say how many of those co-ops would have started anyway.
|
||||
|
||||
So light help is cheap and fades, and heavier help might work but hasn't been tested. That's exactly the kind of question a lottery answers.
|
||||
|
||||
## The founding lottery
|
||||
|
||||
### Who can apply
|
||||
|
||||
- **A group of at least three people** who plan to work in the business, with an idea and a rough sense of what it would take. There's no loan application yet.
|
||||
- **Or a sponsor applying for a group.** Unions, worker centers, community groups and existing co-ops often bring groups together. Many co-ops start that way. They can apply on a group's behalf, and results are reported by who sponsored the group.
|
||||
- **A short screen, run by the reviewers, not the bank,** checks that the people are real and intend to work there, and that the business is legal. Any government ID is enough, including a taxpayer ID number for people without a Social Security number, or a consular or city ID.
|
||||
- **One entry per person for the whole pilot.** Groups that share a member, a family, an address, an idea or an organizer go into the draw as one unit. Groups that lose the draw can't re-enter under a new name.
|
||||
|
||||
### The draw
|
||||
|
||||
The reviewers, not the bank, draw half the groups by lottery, using a method filed in advance. The draw is balanced by industry, region, the founders' wealth and prior earnings, and which sponsor, if any, applied for the group. No one sponsor can enter more than a set share of the groups. As with the support lottery, groups agree to the lottery when they apply, and an independent research ethics board approves the design. Groups that weren't drawn can apply for a loan at any time, and are offered the same help after the year-four measurement if the pilot's help budget has money left. Offering it sooner would give the comparison half help before year four and blur the result the [cost ceiling](#the-cost-ceiling) depends on.
|
||||
|
||||
**The draw doesn't use race or gender.** The rule across the model is that race and sex can be used to measure results, never to decide who gets anything, and the draw decides who gets help. Balancing a lottery on them is ordinary research practice. Federal law even requires government-funded medical research to include women and minorities ([42 U.S.C. § 289a-2](https://www.law.cornell.edu/uscode/text/42/289a-2)), and nobody's odds would change: every group has the same even chance. But a court might still treat it as sorting people by race. The Supreme Court reviews government "distributions of burdens or benefits based on individual racial classifications" under its strictest test ([*Parents Involved v. Seattle School District*, 2007](https://www.law.cornell.edu/supremecourt/text/05-908)), including when a policy "may be said to burden or benefit the races equally" ([*Johnson v. California*, 2005](https://www.law.cornell.edu/supremecourt/text/543/499)), and in 2023 it said "eliminating racial discrimination means eliminating all of it" ([*SFFA v. Harvard*](https://www.law.cornell.edu/supremecourt/text/20-1199)). No court has ruled on a research lottery balanced this way, and sharper statistics are unlikely to count as the compelling reason that test asks for. Sex gets a lighter test, and balancing on it would be easier to defend; it's left out so the rule is the same for both. With about 500 groups, a plain draw rarely leaves the halves more than a few points apart on any group that's a sizable share of applicants, so little precision is lost.
|
||||
|
||||
- **Wealth and earnings are there for their own sake.** They predict whether a group gets a co-op running, and helping founders without money is what this lottery is for. They're taken from tax and wage records where founders allow it, not only from what they say.
|
||||
- **Answering is optional.** Founders' race and gender are asked on a separate form, with "prefer not to say," and the answer changes nothing about the group's chances. The form goes to a separate custodian and stays sealed until the draw has been run on coded IDs and its results published. Refusal rates are reported for each half.
|
||||
- **One draw, no reruns.** The random seed is committed publicly before sign-ups open, and the draw is run once. Mistakes are fixed case by case under a rule filed in advance, never by drawing again.
|
||||
- **The analysis is fixed in advance.** The main result is adjusted for the same things the draw was balanced on, and nothing else, whatever the draw looks like. Results by race and gender are published as estimates, next to the smallest difference each could detect, which for a group making up a fifth of applicants is about 25 points. So they describe who was reached; they can't settle whether help works better for one group than another. A group counts toward a category if at least half its founders report it; mixed groups are reported as their own category.
|
||||
- **Never targets.** No group's odds, help or verdict depends on its founders' race or sex, and none of these figures is a goal.
|
||||
|
||||
### What the helped groups get, for up to a year
|
||||
|
||||
- **A stipend for one founding member** for up to six months, so that one of the group can work on getting the business ready full time. That's how Mondragon did it: it paid one of the founders, not an outsider.
|
||||
- **A feasibility study** by an outside specialist: is there a market, what will it cost, and can it pay a loan?
|
||||
- **Legal setup, bylaws and a business plan,** in the group's own language, with interpreters if needed.
|
||||
- **Specialists come from a vetted panel,** assigned by the reviewers, not picked by the group or by anyone who referred it. No specialist can be a member, a member's relative, or someone who referred the group. For three years they can't sell paid services to a group they helped, the same rule that applies to the support lottery's advisers. No one person or organization can advise more than a set number of helped groups.
|
||||
- **About $40,000 per group in all.**
|
||||
|
||||
### What the other groups get
|
||||
|
||||
The same public help any would-be business owner can already get, like the Small Business Administration's free counseling centers, and the bank's published standards. They can apply for a loan whenever they're ready.
|
||||
|
||||
### What it doesn't change
|
||||
|
||||
Help doesn't lower the bar. Every group applies for its loan through the same underwriting. The help doesn't cover early losses. A co-op that needs money to get through its first years borrows it from the bank at market rates, like any other loan.
|
||||
|
||||
### How big
|
||||
|
||||
About 250 groups in each half, so $10 million for the help. With groups that size, the lottery has an 80% chance of detecting a difference of about 12 percentage points in how many groups end up running a co-op, for example 25% against 37%. Smaller effects might be missed.
|
||||
|
||||
**The money comes from the loan budget.** Of the roughly $300 million set aside for loans, $10 million pays for this help instead, which leaves the pilot's cash cushion and running costs untouched. The pilot's capped help budget is $30 million in all: $20 million after the first loan and $10 million before it ([The Pilot, in Numbers](pilot-spec.md#how-much-money-and-why)). Every dollar counts against the bank in the [money test](pilot-spec.md#the-decision-rule), the same as help after the loan.
|
||||
|
||||
## What gets measured
|
||||
|
||||
- **Are they running a co-op?** The main result: the share of all groups in each half that have a co-op operating two years after the draw, with at least three members and a minimum level of sales, however it was financed. It's measured from government business records by the reviewers, not decided by the bank. So the result can't come from helped groups simply being coached to pass the bank's own underwriting.
|
||||
- **Are they still running at year four?** The share of all groups in each half with a co-op still open and paying fairly four years after the draw. That counts every group, so help that pushes weak groups into launching and failing shows up as a cost, not a benefit.
|
||||
- **What did it cost?** The public cost for each extra co-op the help created, and for each extra co-op still open at year four. At the smallest effect the lottery can detect, about 30 extra co-ops for $10 million, that's roughly $330,000 each. A ceiling is set in advance, [below](#the-cost-ceiling). Above it, the help isn't recommended for any larger program, however many co-ops it creates.
|
||||
- **Who did it reach?** Founders' prior earnings, wealth, gender, race, whether they were born abroad, and language, in each half, from the optional form.
|
||||
- **Where else did groups get help?** Groups report other help and funding they used, so any effect from help found elsewhere, or co-ops simply moving from other lenders to this one, can be estimated.
|
||||
|
||||
### The cost ceiling
|
||||
|
||||
**What counts as the cost.** Everything the public spends in the helped half beyond what it spends in the other half, by year four: the help itself, running the draw and the specialist panel, the help co-ops get after their first loan, and the bank's net result on their loans, losses minus interest earned above the Treasury rate. That's divided by how many more co-ops the helped half has still open and paying fairly ([as the pilot defines it](pilot-spec.md#what-gets-measured)) at year four. Comparing whole halves means nobody has to guess which co-ops were the "extra" ones. The minimum level of sales a co-op needs to count is filed before the draw.
|
||||
|
||||
**The ceiling: $400,000 per extra co-op still open at year four,** in today's dollars, rising with inflation. There's no good benchmark, so here is what the number rests on:
|
||||
|
||||
- **New York City's program** spent about $280,000 for each co-op launched in 2021 ([above](#what-we-know)). That figure is rough both ways. Some of the money went to existing co-ops, which makes it too high. But it counts co-ops at launch, not still open four years later, and it can't tell which would have started anyway, which makes it far too low as a cost per extra co-op. It shows the scale of what a city has spent, not what the help is worth.
|
||||
- **Business tax breaks.** The economist Timothy Bartik finds that state and local tax breaks typically cost about $200,000 for each job that wouldn't have come to the area without them. He puts infrastructure and services in struggling places, counting jobs at local suppliers and shops, at $30,000 to $100,000 a job ([Bartik, 2024](https://eig.org/wp-content/uploads/2024/07/TAWP-Bartik.pdf)). A co-op needs at least three members to count, so $400,000 is about $133,000 per member in the smallest one. That's between his two figures. It isn't a like-for-like comparison: most members would have worked somewhere else, and co-ops' main benefit is ownership and a say, not jobs.
|
||||
|
||||
**It's hard to meet, and that's said now.** With about $11 million spent before the first loan and roughly $50,000 of later help for each co-op, the helped half needs something like 32 or more extra co-ops still open at year four, out of about 250 groups. That's more than the smallest effect the lottery is built to detect, which is about 30 extra co-ops running at year two, before any have closed.
|
||||
|
||||
**Three possible verdicts, set in advance.** With about 250 groups in each half, the estimate will be uncertain, so the report uses a range filed before the draw:
|
||||
|
||||
- **Worth it:** even the high end of the range is under the ceiling. Only then is the help recommended for a larger program.
|
||||
- **Not worth it:** even the low end is over the ceiling.
|
||||
- **Not settled:** anything else. The report says so and gives the size of trial that could settle it, but it doesn't recommend the help.
|
||||
|
||||
**Reported by group, judged as a whole.** The cost per extra co-op is also published by founders' wealth, race and whether they were born abroad, and for struggling areas against the rest. Help that works mainly for low-wealth founders may be worth more than a single national line admits. But the verdict uses the one ceiling, so a favorable slice can't be picked out afterward. The cost is reported again at year six, since the effects of help often fade.
|
||||
|
||||
The co-ops from both halves count in the main test, and the main result is also reported without the helped half's extra co-ops. So help can't quietly change what the main test measures.
|
||||
|
||||
## Should founders put in money of their own?
|
||||
|
||||
Mondragon required it: its founders put in a fifth of the start-up money. Under Italy's Marcora program, which helps workers buy out closing firms, workers put in their own money too, often from their severance pay and unemployment benefits ([Has This Been Tried?](precedents.md)). A stake makes founders care what happens to the loan. But a cash buy-in shuts out workers with no savings, who are the people co-ops most often serve.
|
||||
|
||||
**The model's answer: a small stake, payable in work.** The founding members together put in at least 5% of the first loan. They can pay it in cash, in work, or both.
|
||||
|
||||
- **Work is valued at the local median wage for the job**, from the government's wage survey, not at whatever pay the founders plan to give themselves.
|
||||
- **Only hours logged at the time count,** from the date of application, and a specialist from the panel or the reviewers checks them. Hours paid by the stipend don't count.
|
||||
- **Only work before the co-op is formed can be unpaid.** After that, founders are its employees and are paid at least the minimum wage. Only the difference between the minimum wage and the median wage counts toward the stake ([Owners Are Still Workers](labor-law.md#founders-unpaid-work)).
|
||||
- **It's a real stake.** Unpaid work is recorded as back pay the co-op owes, ranked behind the bank ([The Founder Problem](founder-problem.md#the-tools)). Founders lose it if the co-op fails.
|
||||
|
||||
For a typical first loan of $500,000, that's $25,000 in all. Five founders could meet it with about $5,000 each, or with a few months of evenings and weekends. Three founders would need about $8,300 each, or more hours.
|
||||
|
||||
**What the stake does and doesn't do.** It shows commitment, and it makes sure founders have something to lose. It doesn't stop fraud. A group that plans to take the money and disappear gives up only work it never intended to be paid for. The defense against that is elsewhere: loans paid out in stages as the business hits milestones, and the rules in [Guarding the Pilot](pilot-safeguards.md).
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **It tests help for groups that come forward.** Many workers never think of starting a co-op at all. Sponsors like unions and community groups help reach them, and results are reported by sponsor. But the lottery can't test outreach to people nobody reaches.
|
||||
- **A year's help may not be enough.** New York's partners often work with groups for several years. If the effect needs longer, this design will understate it.
|
||||
- **Groups that lose the draw may find help elsewhere.** That makes the helped half look less different, so the result may understate the real effect, as happened in GATE.
|
||||
- **Four years of survival is a small sample.** Only the groups that launch can survive, perhaps 60 to 90 in each half. So the year-four result will be rough.
|
||||
- **The work stake still costs time.** Founders who can't spare unpaid hours, because they need every paid hour they can get, still face a barrier. The stipend in the helped half partly makes up for that. Outside it, nothing does.
|
||||
- **Help only for co-ops will be called unfair competition.** It's a research cost, time-limited to the pilot, published each year, and counted against the bank. That's the honest answer. It won't stop the attack.
|
||||
- **$10 million is $10 million.** If the help doesn't create more co-ops that last, the pilot will have spent it to learn that.
|
||||
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: When an Owner Wants to Sell to the Workers
|
||||
section: Growing the Sector
|
||||
order: 60
|
||||
summary: Owners who choose to sell their company to its workers can get a loan from the bank to do it, on a separate track with its own money and its own test, so the sale can't overcharge the workers or muddy the main experiment.
|
||||
---
|
||||
|
||||
# When an Owner Wants to Sell to the Workers
|
||||
|
||||
This model buys nobody's company. But some owners *want* to sell to their workers. Most often, they're retiring, have no family member who wants the business, and would rather it stay in the town with the people who built it than go to a competitor or a private equity fund.
|
||||
|
||||
American banks already lend for some of these sales, as the evidence below shows. But those are mostly sales to employee stock ownership plans (ESOPs), trusts that come with large federal tax breaks and don't give workers one member, one vote. Sales to a real worker co-op, especially of smaller firms, are harder to finance. Lenders usually want personal guarantees, which a whole workforce can't give ([Why a new bank](pilot-spec.md#why-a-new-bank-not-existing-programs)).
|
||||
|
||||
So should [the bank](the-bank.md) lend for these sales? This page's answer is **yes, but on a separate track**, with its own money, its own rules and its own test, and alongside private lenders wherever they'll join in. The evidence for buyouts of healthy firms is among the strongest in the co-op literature. The ways they go wrong are also well known, and every rule below answers one of them.
|
||||
|
||||
## The case for it
|
||||
|
||||
**Healthy firms sold to their workers survive.** In France, five years after a healthy business is handed over to its workers as a co-op, 90% are still running. For French co-ops as a whole the figure is 79%, and for French businesses generally it's 61%. Even struggling firms taken over by their workers reach 76% (France's co-op federation, cited in a [2025 report to the French National Assembly](https://www.assemblee-nationale.fr/dyn/opendata/RINFANR5L17B1696.html)). That isn't a like-for-like comparison. The general figure includes brand-new businesses, and firms chosen for a handover were already doing well. Still, it's hard to read as a warning.
|
||||
|
||||
**Loans for these sales get repaid.** A study of 1,232 bank loans for ESOP buyouts at three large banks, from 2009 to 2013, found only 0.2% a year defaulted in a way that cost the lender money. By comparison, loans to mid-sized companies generally defaulted at 2% to nearly 4% a year over similar periods, though the two figures don't count defaults in quite the same way ([NCEO, 2014](https://www.nceo.org/hubfs/assets/pdf/Default-Study-full.pdf)). So the comparison only suggests something. It doesn't prove it.
|
||||
|
||||
**It can grow the sector fast.** Britain created a simple way for owners to sell to a trust for their employees in 2014, with a tax break. By January 2025 there were about 2,000 of these employee ownership trusts, and one estimate puts them at 6% of all business sales in 2024. Most are doing well. Very few have failed, mostly in construction, where profit margins are thin ([Pendleton and Robinson, 2025](https://eprints.whiterose.ac.uk/id/eprint/223994/)).
|
||||
|
||||
**It reaches workers the startups don't.** A startup co-op begins with a handful of people. A buyout brings in a whole existing workforce at once, often in trades where new co-ops rarely start.
|
||||
|
||||
**A caution about all of this evidence.** None of it comes from exactly this design. ESOPs and British trusts come with big tax breaks, and neither gives workers one member, one vote. Italy's buyouts under the Marcora Law also show high survival, 87% still operating, but most were rescues of firms in crisis, and many were only a few years old when counted ([Has This Been Tried?](precedents.md#italys-co-op-funds-coopfond-and-the-marcora-law)). The evidence shows that selling a healthy firm to its workers can work. It doesn't show that this particular track will.
|
||||
|
||||
## What goes wrong
|
||||
|
||||
**Workers overpay.** The seller usually knows the business best and wants the highest price. In one American case, the trustee who was supposed to protect the workers approved a price after a half-hour meeting about the valuation, and the workers' plan overpaid for Constellis, a private security contractor, by almost $30 million. A federal appeals court upheld the judgment ([Brundle v. Wilmington Trust, 4th Cir. 2019](https://www.ca4.uscourts.gov/opinions/171873.P.pdf)). In a British case, a founder gave his adviser rosy forecasts, the adviser raised them further, and the managers who had to run the company were given no time to read the terms. The company nearly failed under its payments to the founder and was saved only by renegotiating (Pendleton and Robinson).
|
||||
|
||||
**The seller keeps control.** Under Britain's original rules the seller could appoint the trustees who were meant to act for the workers. Britain changed that in 2024. Now the seller and their associates can't make up half or more of the trustees, and the trustees must make sure the price is no more than market value ([Pendleton and Robinson](https://eprints.whiterose.ac.uk/id/eprint/223994/)).
|
||||
|
||||
**The debt comes out of the workers' future.** Almost nine in ten British trust sales are paid for by the company itself, out of future profits, over about six years (Pendleton and Robinson). That's the workers' money, paying the seller. Most firms manage it, but it can squeeze out investment and pay.
|
||||
|
||||
**Tax breaks get expensive and attract the wrong sellers.** Britain let owners who sold to a trust pay no capital gains tax at all. By 2021–22 the tax break cost £600 million a year, and it was forecast to reach £2 billion by 2028–29. One accounting firm puts that at about 20 times what was originally expected ([Scholes Chartered Accountants, 2025](https://www.scholesca.co.uk/blog/why-did-the-autumn-budget-change-employee-ownership-trusts)). In November 2025 the government cut it back, so that only half of the gain is tax-free ([Charles Russell Speechlys, 2025](https://www.charlesrussellspeechlys.com/en/insights/expert-insights/corporate/2025/employee-ownership-trusts---government-reduces-capital-gains-tax-relief-on-employee-ownership-trusts-in-2025-budget/)). The researchers noted that a big tax break "could attract owners who have little or no interest in employee ownership."
|
||||
|
||||
**A trust isn't a co-op, and it isn't forever.** In a British trust, the workers usually don't vote one member, one vote. The company is typically run with an ordinary management hierarchy, and a trust can later sell the company. Nothing like [the lock](the-lock.md) stops it.
|
||||
|
||||
**Rescues of failing firms lose money.** Uruguay's public fund lost almost 90% of its loans, mostly on firms that had already failed ([The Pilot, in Numbers](pilot-spec.md#two-warnings-from-real-life)).
|
||||
|
||||
## The rules
|
||||
|
||||
### Healthy firms, willing sellers, no new tax break
|
||||
|
||||
- **Only healthy businesses.** At least three years of profits, with the latest year audited by an outside accountant and every year matching the tax returns. Sales to the seller's own other businesses or relatives don't count toward those profits. The seller must have owned the business for at least three years, and it can't be in bankruptcy or behind on taxes. Rescues stay out, as they do for [the rest of the pilot](pilot-spec.md#who-can-borrow).
|
||||
- **The owner starts it, in writing.** The bank never approaches an owner, and neither do the advisers or groups the pilot pays. The owner can back out at any time before closing.
|
||||
- **No new tax break.** The bank's only offer is that it will lend. That keeps out most owners who are only after a tax deal, and it keeps the cost to the public from growing the way Britain's did. An old tax break already covers some of these sales. The model leaves it alone, but counts its cost against the track ([below](#the-tax-break-that-already-exists)).
|
||||
- **Private lenders go first.** The bank lends alongside a private or community lender, such as a bank, a credit union or a community development lender like the co-op loan funds, that takes at least a quarter of the main loan, on the same terms and in the same place in line. That's demanding, and it's meant to be. In the conversions Project Equity reviewed in 2017, the main loan was usually shared among several lenders, often led by a small co-op loan fund, and some bigger lenders would join only in the safest, first-paid slice of the larger deals ([Project Equity, 2017](https://www.project-equity.org/wp-content/uploads/2023/06/Addressing-the-Risk-Capital-Gap-for-Worker-Coop-Conversions_Strategies-for-the-Field_Project-Equity.pdf)). The bank won't take the riskier slice behind a private lender, because that would put the public's money where the private money should be.
|
||||
- **The private quarter has to be really private.** It counts only as far as the lender keeps it at risk: not guaranteed or bought in part by a state or federal program, and not sold on, for the whole life of the loan, certified each year. Only new money counts, so a seller's existing lender can't join at a quarter while the sale pays off its old, bigger loan. A lender funded by the bank itself doesn't count. Every lender says in the deal file where its own money comes from, and the bank reports each year how much of the private share came from ordinary banks and credit unions and how much from mission lenders, some of which are partly funded by public programs or charities.
|
||||
- **The lenders share alike.** A written agreement among the lenders, published with the deal, splits fees, collateral and any early repayment in proportion. The bank or a neutral party manages the loan, so the private lender can't collect first or push a piecemeal sale that suits it, and every step in a default follows [the lock](the-lock.md).
|
||||
- **When no private lender will join.** The bank can lend alone only after two lenders with no ties to the deal have turned down an equal quarter share on the standard form, naming the terms they would have accepted. Deals the bank makes alone are reported separately, with their own loss rate, and can't be more than a third of the track's loan money. If that cap is reached, the bank stops lending alone and says so. The bank expects many deals to need it, because an equal place in line is a strict rule. A high share of solo deals would say more about how strict the rule is than about whether the co-ops can pay. If private lenders join almost every deal, that's a sign the bank isn't needed for buyouts, and the results will show that too.
|
||||
|
||||
### The tax break that already exists
|
||||
|
||||
Since 1984, federal law has let an owner put off capital gains tax on selling a company's shares to an ESOP or a worker co-op ([26 U.S.C. § 1042](https://www.law.cornell.edu/uscode/text/26/1042)). The rules:
|
||||
|
||||
- **The company** has to be an ordinary "C corporation" whose shares aren't publicly traded, and the sale has to be of its shares, not its assets.
|
||||
- **The seller** has to have held the shares for three years, and the co-op has to own at least 30% of the company right after the sale.
|
||||
- **The money** has to be reinvested in shares or bonds of other American operating companies, between three months before the sale and a year after it. Only the part reinvested in that window is covered.
|
||||
- **The tax** comes due when the seller sells that replacement investment. Giving it away doesn't trigger the tax. If they hold it until they die, their heirs inherit it at its value then, and the tax is never paid. Sellers commonly buy long-term bonds as the replacement and borrow against them, so they get the cash without the tax ([CSG Partners, 2023](https://www.csgpartners.com/esop-resources-news/1042-rollovers-capital-gains-taxes-esop); [26 U.S.C. § 1014](https://www.law.cornell.edu/uscode/text/26/1014)).
|
||||
|
||||
**The limit to C corporations is weaker than it looks.** An owner of an S corporation, the more common kind among small companies, can drop that status before the sale, even the day before ([Wiener, 2021](https://jrwiener.com/updated-section-1042-tax-benefits/)). The cost is waiting five years to switch back ([NCEO](https://www.nceo.org/employee-ownership-faq/who-qualifies-for-the-section-1042-tax-deferral-esop)), and a locked co-op is a C corporation anyway ([How a Locked Co-op Is Taxed](taxes.md)).
|
||||
|
||||
**It has barely been used for co-ops.** A guide for owners posted in 2011 said that "as far as the we can determine, that provision has never been used" for co-ops, though sales to ESOPs use it routinely ([Britton & Stewart, NoBAWC](https://nobawc.org/wp-content/uploads/2011/11/Selling-to-Your-Employees-through-a-Worker-Cooperative.pdf)). That's old and hedged, and nobody seems to have counted since. The authors think a company can turn itself into a co-op and buy back the owner's shares, and still count as a sale to a co-op. That hasn't been tested either. A bank that lends for co-op buyouts could make the break far more common, and that makes its cost part of what the track costs.
|
||||
|
||||
**The co-op takes on risks too.** To give the seller the break, the co-op has to sign a statement agreeing to two federal penalty taxes (§ 1042(b)(3)):
|
||||
|
||||
- **10% of the sale price** if it sells the shares it bought within three years and its holding falls ([§ 4978](https://www.law.cornell.edu/uscode/text/26/4978)). A forced sale after a default in those years could trigger it.
|
||||
- **50%** if any of those shares, or the benefit of them, goes to the seller, their relatives or anyone owning a quarter of the company ([§ 4979A](https://www.law.cornell.edu/uscode/text/26/4979A)). A seller or relative who works there can still be a member on the same terms as anyone else, but shouldn't get any of the shares the seller sold ([Britton & Stewart](https://nobawc.org/wp-content/uploads/2011/11/Selling-to-Your-Employees-through-a-Worker-Cooperative.pdf)). How that fits with members' capital accounts is a question for each co-op's lawyer.
|
||||
|
||||
A share sale also brings every old debt and lawsuit of the company along, and the co-op doesn't get the higher tax value on its equipment that buying the assets would give it, which means more tax later.
|
||||
|
||||
**What the model does about it:**
|
||||
|
||||
- **It doesn't forbid it, or reward it.** The model doesn't change tax law in either direction. The bank can't refuse or reprice a deal because the seller takes the break, or because they don't.
|
||||
- **The co-op decides with its own lawyer.** Before it signs the statement, the co-op gets written advice from its own lawyer on the penalty taxes and the share-sale risks, paid from the help budget. Any penalty tax the co-op owes because of the seller's break comes out of the seller's paid-last share first. The co-op can sign only at closing, with a copy to the bank. Signing later without telling the bank breaks the loan terms.
|
||||
- **The seller proves it.** A seller who takes the break sends copies of the election and the proof of reinvestment within 15 months of the sale. Part of their paid-last share is held back until they arrive. The reviewer checks them.
|
||||
- **It's counted as tax not collected, in the pass test.** The cost is the federal tax the seller would have owed on the sale without the election, at the seller's own rate, leaving out gain another rule already excuses, such as the small-business stock exclusion ([§ 1202](https://www.law.cornell.edu/uscode/text/26/1202)). It's counted as never paid. It goes into the track's [money test](#why-a-separate-track) like any other cost, because the track is what makes it happen. Lost state tax is reported beside it but not counted, since it isn't federal money.
|
||||
- **The deal file shows it next to the price.** Workers and their advisers see what the seller's tax saving is worth before the vote. They can bargain over it. The model doesn't set a split. It's also in the seller's line in [Where the money went](#who-buyouts-are-compared-against).
|
||||
|
||||
**How big it could be.** Take the track's typical $2.5 million sale, where about $2 million is paid at closing and the rest through [the seller's share](#the-seller-shares-the-risk). If the owner started the business, nearly all of the price is gain. At the top rate of 23.8% (20% capital gains plus the 3.8% investment income tax), the tax put off on the closing money is about $475,000. It's up to about $600,000 if the seller reinvests other money to cover the rest. That's a third or more of the bank's roughly $1.5 million loan. If every one of 50 to 60 deals used it, that's roughly $25 million to $35 million, a quarter or more of the track's money. If many sellers take the break, the track will probably fail its money test. That's the honest answer to whether these buyouts paid the public back. Nobody knows how many will take it, because almost nobody has.
|
||||
|
||||
### A fair price
|
||||
|
||||
- **Appraisers come from a panel, in rotation.** An independent appraiser, drawn in turn from a panel the bank keeps and paid by the bank, values the business under standard professional rules. The value is what the business is worth on its own, to a buyer who'll run it as it is. It doesn't include the premium a competitor might pay to combine it with their own business, since the workers can't get that value out of it.
|
||||
- **Profits are checked, not taken on trust.** Before the appraisal, an outside accountant restates the profits as they'd look under new owners. The owner's own job is counted at a normal salary for that work. Upkeep and equipment the owner put off are counted as costs. One-off gains are taken out.
|
||||
- **The seller can challenge it.** The seller can order a second appraisal from the panel, at the seller's own cost, so asking again isn't free. If the two are within 10% of each other, their average is the final figure. If they're further apart, a third panel appraiser settles it, and that figure is final. The third appraisal is paid for by whichever side's appraisal it lands further from, and if it lands nearer the seller's, the bank also pays back the seller's second appraisal. The price can't be higher than the final figure.
|
||||
- **Appraisers are checked too.** One deal in ten, picked at random, is re-appraised after closing by a different appraiser. Appraisers whose values run consistently high are dropped from the panel.
|
||||
- **Everything the seller gets counts as the price.** For five years after the sale, every payment to the seller, their family or their other companies counts toward the price: salary, consulting fees, rent, royalties, payments for not competing. The only exception is a payment set at a going rate by an independent appraiser. If the seller keeps the building, the co-op gets a lease of at least ten years at appraised rent and the right to buy it at appraised value. Patents, brands, customer lists and a normal level of working cash all come with the business.
|
||||
- **The seller tells the truth, in writing.** The seller discloses in writing any big customer or contract that's at risk, any lawsuit, and anything else a buyer would need to know. If the disclosure turns out to be false, the co-op can cut what it still owes the seller by the damage, and sue for more.
|
||||
- **The workers get their own adviser**, chosen by the workers (or their union, if they have one) from a list the bank approves, and paid a flat fee from the buyout track's own budget, the same whether or not the sale goes through. The workers can replace the adviser by majority vote. Each adviser's record, how many of their deals closed and how those firms fared, is published, and advisers aren't handed more work for closing more deals. The adviser explains the price, the debt and the risks in plain terms. The workers get at least 30 days with the full terms before they vote.
|
||||
|
||||
### The seller shares the risk
|
||||
|
||||
- **At least 20% of the price is paid to the seller over time**, and only after the bank's loan payments are made. If the price was too high and the business struggles, the seller's payments stop before the workers' jobs do.
|
||||
- **Nothing is paid on it for the first two years.** Interest builds up, but the seller's payments start only in year three. The workers here put almost nothing in up front, so the seller's waiting share, a loan paid last, is what stands between the lenders and a loss. It isn't the same as workers' own money. The SBA doesn't make sellers wait at all, but it counts a seller's loan as part of the buyer's own stake only if nothing is paid on it for the whole life of the SBA loan ([SBA SOP 50 10 8.1](https://legacy.sba.gov/sites/default/files/2026-09/SOP%2050%2010%208.1%20Technical%20Policy%20Updates%20effective%2010.1.2026.docx)). Two years is far short of that, so this is a loan paid last, not a stand-in for workers' cash. Two years is meant to cover the first, riskiest stretch after the sale without asking a seller to wait ten. After that, the lenders' protection gets thinner each year as the seller is paid. The coverage test and the forecast cut below only partly make up for it: they check that the business can pay, but they aren't a cushion if it fails. What the lenders do have in front of them is a smaller loan. The main loan here is about 80% of the price, where an SBA lender can lend up to 90%. Anything else paid to the seller in those two years, such as adviser's fees, waits with it and is paid last too.
|
||||
- **The seller's loan has limits of its own.** Its interest rate can't be higher than the bank's own rate on the deal plus 4 points. That's a higher rate than the bank gets, because the seller is paid last and waits, but it's capped: any interest above the cap counts as part of the price, so it can't be used to slip past the appraisal cap. The deal file states what the seller's loan is really worth at a going rate for loans paid last, so everyone can see how much of the price is money now and how much is a promise. The loan is due within ten years of the sale, extended by any time its payments had to wait (below), with interest still building at the capped rate. A cut for a wrong forecast (next point) applies to the interest that has built up too, not just the original amount.
|
||||
- **That share shrinks if the forecast was wrong.** If checked profits over the first three years average more than 20% below what the appraisal assumed, the price is treated as too high by the shortfall beyond 20%, and that amount comes off what's still owed to the seller, up to all of it. On a $2.5 million sale with $500,000 still owed, profits 30% below the forecast cut $250,000 (10% of the price); profits 40% or more below cut the whole $500,000. The cut is measured against the whole price, not just the seller's remaining share, because a seller who knew a drop was coming was paid most of the inflated price in cash at closing. There's no cliff, so a few dollars of profit either side of the line don't swing the seller's payout, and nobody on either side has much reason to nudge the books.
|
||||
- **Neither side can tilt the test.** Profits for the test are counted under the same accounting rules as the pre-sale restatement, with all pay, members' and managers' alike, held at pre-sale rates per unit of work (after inflation), and upkeep at its normal level. So the co-op can't shrink the seller's share by raising pay, hiring ahead, skipping repairs or moving costs into the test years. If profits jump back in year four, costs shifted into the test years are added back. The seller gets the audited accounts and the calculation, and either side can send a dispute to an accountant from the bank's panel, whose answer is final. The seller's payments due in year three wait in a separate account until the test is settled.
|
||||
- **Only half of an industry-wide fall is forgiven.** A fall that hits the whole industry is measured by a public index, chosen for each industry in the lending standards filed before the first loan, not picked deal by deal. Which industry a firm is in is fixed at signing, from the industry code on its last tax return before the sale. Half of that fall is taken out before the test, and half stays with the seller. A seller shouldn't carry all of a recession they couldn't have known about, but the workers shouldn't carry all of it either. The adjustment works both ways: if the industry grew, the forecast is raised by half the growth before the test.
|
||||
- **The seller is protected like a lender, and only like a lender.** The seller's share carries a legal claim on the business's assets, behind the bank's, and the right to be paid. It carries no say in how the co-op is run. The seller signs a written agreement with the bank and the private lender to be paid behind them, and can't demand early repayment or seize assets while the bank's loan is unpaid. If the seller sells the loan to someone else, every rule here, the wait, the cut and the pay protection, binds the new holder.
|
||||
- **Pay isn't cut to pay the seller.** In any year the co-op cuts its members' pay rates below where they were before the sale (after inflation), the seller's payments wait. They're owed later, not cancelled, and the due date moves back by the same time, so the waiting payments don't pile up into one lump at year ten.
|
||||
- **The debt has to fit the business.** The bank lends only if the business's checked cash flow, what's left after normal upkeep spending and taxes, covers all the loan payments, the bank's, the private lender's and the seller's, plus the cash members need to pay tax on their profit shares and the capital-account payouts that will come due ([Member Capital Accounts](capital-accounts.md#where-each-years-profit-goes)), with a real margin to spare: checked cash flow in each of the last two years, and on average over the last three, at least 1.25 times the payments in the heaviest year of the schedule. That's usually year three, when the seller's payments start on top of everything else, with the interest that built up during the wait. The SBA asks the same 1.25 times for a co-op or ESOP buying a business, under the rules that take effect in October 2026, measured on the last year or the average of the last two, with no credit for forecasts. It counts a seller's loan too, unless nothing is paid on it for the whole life of the SBA loan, and it treats a loan with a payment holiday as if it were repaid over ten years or less ([SBA SOP 50 10 8.1](https://legacy.sba.gov/sites/default/files/2026-09/SOP%2050%2010%208.1%20Technical%20Policy%20Updates%20effective%2010.1.2026.docx)). So neither the margin nor counting the seller is unusual. The bank's test is stricter in three narrower ways. It counts cash left after upkeep and taxes, where the SBA starts from earnings before interest, taxes and depreciation, adjusted as the lender judges prudent. It tests the heaviest year of the schedule, not an average. And it counts the cash members need for tax on their profit shares and their account payouts. The SBA also caps all the debt for a purchase at the business's appraised value, as this track does. The margin is part of the lending standards filed before the first loan ([Making It Last](durability.md#what-a-new-board-cant-change)).
|
||||
|
||||
### The workers decide
|
||||
|
||||
- **Who votes.** Every worker on the payroll 12 months before the owner applied, and still there, can vote, including part-time workers and agency temps who work there regularly. The seller and their family can't. Nobody can be laid off, reclassified or offered money tied to how they vote in that time.
|
||||
- **How they vote.** By secret ballot, run by an independent election body, with a neutral ballot and after the 30 days with the full terms. The sale needs a majority of *everyone eligible*, not just of those who turn out.
|
||||
- **Unions keep their place.** A union contract carries over to the co-op. Becoming a member doesn't take away anyone's right to organize.
|
||||
|
||||
### Then they govern
|
||||
|
||||
- **Everyone becomes a member unless they say no.** At closing, every worker is enrolled as a member unless they opt out in writing. The buy-in is kept small and can be paid by payroll deduction, so no one's shut out for lack of savings. Workers who opt out can't be fired for it, and can join whenever they like.
|
||||
- **The membership rule, with a fair start.** New hires follow [the lock](the-lock.md)'s usual rule: membership after at most 12 months. For the first three years, workers who were there before the sale and chose not to join don't count against the rule that members must do three-quarters of the work hours. After that, the normal rule applies, as it does for every co-op the bank funds.
|
||||
- **One member, one vote, from day one.** A converted firm meets the full [co-op definition](worker-ownership.md), unlike a British trust. It can keep managers, but they answer to a board elected by the members. The first board election happens within 90 days of closing.
|
||||
- **The seller's control ends at the sale.** The seller can stay on as a paid adviser for up to two years, and the members can end that by vote at any time. If the seller works there, they can join as a member, with one vote like anyone else. Neither they nor their family or associates can sit on the board, hold a special share or choose who governs.
|
||||
- **Paying off the debt builds the members' accounts.** Loan payments come out of profit, but they aren't a cost in the co-op's books. So the profit that pays down the loan is still credited to members' [capital accounts](capital-accounts.md) under the usual split, and paid out on the usual schedule. The workers who pay for the business build up a claim on it.
|
||||
- **Help with the change.** Moving from one owner to a members' democracy is hard. The French parliament's report calls support and training for newly elected leaders essential. Every converted firm gets the standard help new co-ops get, including governance training for its elected leaders. Half, chosen by lottery, also get the intensive help that [the support lottery](pilot-spec.md#the-support-lottery) tests. Both are paid from the buyout track's own budget.
|
||||
- **Who benefits is published.** Who became a member, who sits on the board and how profit shares are split, by race, gender and job, the same as for [the startups](pilot-spec.md#what-gets-measured).
|
||||
|
||||
### Locked, like every co-op the bank funds
|
||||
|
||||
A converted firm accepts [the lock](the-lock.md) and stays a co-op forever. This is the biggest difference from Britain's trusts. The seller gets a fair price once. After that, nobody can cash in the business again. Every seller signs knowing that.
|
||||
|
||||
The lock costs the seller something too, and they should know it. If the co-op fails while the seller is still owed money, the business can't be sold whole to an ordinary buyer. It can only go to another co-op, or be sold off in pieces. So the seller will likely get back less than from a failed conventional firm. That's one more reason to set the price where the business can carry it.
|
||||
|
||||
## Why a separate track
|
||||
|
||||
Buyouts would make the pilot's main result look better than it deserves. A business that has already survived for years is far more likely to keep surviving than a new one. If buyouts were mixed into the main test, the co-ops' survival would look better whether or not worker ownership had anything to do with it. That's why [the pilot](pilot-spec.md#who-can-borrow) already scores existing firms separately.
|
||||
|
||||
So buyouts get:
|
||||
|
||||
- **Their own money.** $100 million, set aside in the same one-time charter, separate from the $350 million for new co-ops. That raises taxpayers' worst case from $350 million to $450 million, and the model says so. $12 million of it pays for the workers' advisers, the checks on deals that don't close and the help after a sale ([What the help costs](#what-the-help-costs)). Buyout loans can never draw on the startup money or the startup help budget, so they can't shrink the main test.
|
||||
- **Their own limits.** The same $3 million lifetime limit, which covers every business owned by the same seller or group, so one owner can't split a company into pieces to get around it. The same caps on industries and places apply. With a private lender's share and the seller's share on top, that means companies priced up to about $5 million: small and mid-sized firms. Bigger sales can use the ESOP market that already exists.
|
||||
- **Their own test, judged at year 12 with the rest.** It has two parts, and both must pass:
|
||||
1. **The money.** The buyout money has to earn at least the Treasury rate after all losses and costs, including the advisers' budget, the appraisals, the track's share of running the bank, and the federal tax sellers put off under the [old tax break](#the-tax-break-that-already-exists). Its losses can't be clearly higher than the loss rate the bank publishes before its first buyout loan ([below](#who-buyouts-are-compared-against)). Deals the bank made alone count, and so do loans restructured to keep them alive, under the same rules as the startup test ([Keeping zombies alive](pilot-safeguards.md#keeping-zombies-alive)).
|
||||
2. **The pay.** The test follows everyone who was on the payroll at the sale, members or not, whether their firm is still open or not. Their median pay per hour, after inflation, can't have fallen below what it was over the last two audited years before the sale. Workers whose firm closed count at what linked government wage records show they earn now, and as below the line where no record can be found. A track that gets repaid by cutting pay, or by letting the lowest-paid go, hasn't passed.
|
||||
|
||||
The two tracks pass or fail on their own. A good buyout record can't rescue a failing startup test, and a bad one can't sink a passing one.
|
||||
|
||||
### Why not just use the SBA?
|
||||
|
||||
The SBA can already back a loan for a co-op to buy a controlling share (51% or more) of a business. But its rules, including the revision that takes effect on October 1, 2026, treat a co-op worse than an ESOP in two ways that matter for a buyout ([SBA SOP 50 10 8.1](https://legacy.sba.gov/sites/default/files/2026-09/SOP%2050%2010%208.1%20Technical%20Policy%20Updates%20effective%2010.1.2026.docx)):
|
||||
|
||||
- **Cash up front.** Buyers in a change of ownership have to put in at least 10% of the cost themselves. An ESOP buying control is exempt. A co-op isn't. The 2026 rules let a lender cut or waive the cash for an "owner buyout," which can include current employees buying the business as individuals, but only if the buyers already have enough cash and working capital. A co-op buying control is its own category, with no waiver. And workers who bought as individuals would each be owners who have to guarantee the loan, which is the next problem. A seller's loan can cover up to half of that, but only if nothing at all is paid on it until the SBA loan is repaid. So the workers would still have to find at least 5% of the project's total cost in cash, including fees and any working capital, and more if the seller won't wait that long. For a $3 million business, that's $150,000 or more, from people most of whom don't have it.
|
||||
- **Someone has to guarantee it.** The SBA's rule is that owners of 20% or more guarantee the loan. For an ESOP, the rules say plainly that the workers don't have to. For a co-op there's no such exception, and a second rule applies: if nobody owns 20% or more, which is normal in a co-op with more than a handful of members, "at least one of the owners must provide a full unconditional guaranty." That means asking one worker to promise their house and savings for everyone's loan. Lenders have long asked the same of co-ops in practice, and co-op advocates and a House committee have pointed to it as a reason co-ops have rarely used the SBA since Congress opened it to them in 2018 ([The Pilot](pilot-spec.md#why-a-new-bank-not-existing-programs)).
|
||||
|
||||
Since the 2026 rules, those two are close to the whole gap. The SBA now has a category for co-op purchases, asks them for the same margin as everyone else, and doesn't require an extra earnings review. For a $3 million business, what's left is about $150,000 of cash, roughly $5,000 each for 30 workers, and one member's signature. A narrower fix could close that: the rule change above, or a small public fund that gives the cash stake as a grant, or lends it on hold for the life of the SBA loan, which the SBA counts as the buyer's own money. The case for putting $100 million into this track rests on those fixes not coming, or on the SBA route still failing co-ops once they do. The track's yearly reports are meant to show which.
|
||||
|
||||
Until then, the bank's track replaces both. The seller's waiting share, paid last, takes the place of the workers' cash. The bank lends without personal guarantees, relying instead on the seller's share, the coverage test and a private lender taking part, and pricing what's left of the risk into its rate. That's a real risk the SBA's rules are there to guard against, and the track's loss rate is how it's judged ([below](#who-buyouts-are-compared-against)).
|
||||
|
||||
To make sure the bank isn't just taking deals the SBA would have done anyway:
|
||||
|
||||
- **Its rates are published next to the SBA's.** Each year, the bank publishes its average rate on buyout loans alongside the typical rate on matched SBA change-of-ownership loans, taken from the SBA's own loan data. The bank will usually charge more. The gap is roughly what doing without the workers' cash and a worker's house costs, along with the bank's own costs.
|
||||
- **Ordinary lenders have to say no first.** Before the bank lends, the co-op must have asked two lenders with no ties to the deal and a real record of lending for business purchases, one of them a conventional lender and not only the SBA route, and been turned down on a standard form that says why. The forms are published with the deal file.
|
||||
- **It counts its own overlap.** The bank publishes each year how many of its deals could have met the SBA's terms, by a definition fixed in the lending standards before the first loan.
|
||||
- **It asks for the SBA fix first.** Both rules are the SBA's own policy, not written into law. Its regulation says only that owners of 20% or more "generally" guarantee ([13 CFR 120.160](https://www.ecfr.gov/current/title-13/section-120.160)). So the model asks the SBA to give co-ops buying control the same two exemptions ESOPs already have, and Congress if the SBA won't. If that happens, the buyout track lends only where the SBA route still can't serve the deal, says each year how many that is, and returns money it no longer needs to Treasury.
|
||||
|
||||
### Other public help that already exists
|
||||
|
||||
The SBA isn't the only public program that can help. Two others already allow worker buyouts, in pieces. Both are as of 2026, and both can change:
|
||||
|
||||
- **State programs paid for by Treasury.** The State Small Business Credit Initiative (SSBCI) gives states federal money to share the risk on small business loans: by buying part of a loan, guaranteeing part of it, or putting up cash to cover a shortfall in collateral. Its rules generally bar using that money to buy out an owner. But they make an exception for a sale to a worker co-op, ESOP or employee trust, as long as the workers end up owning a majority. The private lender has to keep at least 20% of the risk on each loan, and the borrower's own money doesn't count toward that. Treasury's rules ask for personal guarantees only in one narrow case, a loan to a company that just owns the building the business uses. Each state designs its own program, usually with the lender enrolling the loan, and not every state's is open to buyouts. The current round is mostly one-time money from 2021, released in stages, and much of it may be committed before the bank lends ([Treasury guidelines, 2024](https://commerce.mt.gov/_shared/business/MTSSBCI/docs/forms/SSBCI-Capital-Program-Policy-Guidelines-Revised-10.4.24.pdf); [Treasury fact sheet, 2023](https://home.treasury.gov/system/files/136/SSBCI-ESOP-Fact-Sheet.pdf)).
|
||||
- **USDA's rural business guarantees.** The Department of Agriculture guarantees business loans in rural areas. It asks for personal guarantees from anyone who owns 20% or more of the borrower, at least in proportion to what they own. In a co-op with more than a handful of members that's usually nobody, but USDA can also ask smaller owners to guarantee when it judges the risk high enough, and it can grant exceptions. Its loans for turning a business into a co-op or ESOP are capped at $600,000 and 10 years, and must come within five years of the first transfer of ownership, so they cover only a slice of a typical deal ([7 CFR 5001.204 and 5001.140(d)](https://www.ecfr.gov/current/title-7/subtitle-B/chapter-XLII/part-5001)).
|
||||
|
||||
Neither gets around the SBA's rule. An outside guarantee fund doesn't meet it either, because the SBA asks for a guarantee from one of the *owners*. Where these programs do exist, the bank's track uses them, carefully:
|
||||
|
||||
- **The state gets asked first.** Where a state's program is open to buyouts, the private lender is asked in writing to enroll the loan before the bank lends. If the state says no, or hasn't answered within 45 days, that counts as a no, so a slow agency can't run out a seller's clock. The bank steps back only if the private lender plus the state can do the whole deal with no personal guarantee, no more cash from the workers and no higher total cost. Deals it steps back from are reported, and followed, as a group of their own.
|
||||
- **Never with a personal guarantee.** The bank never joins a deal where any program requires a member to guarantee the loan personally.
|
||||
- **Help is counted as the public's money.** State or USDA help can sit alongside the bank's loan, for example to cover a collateral shortfall. It never counts toward the private quarter. The track's loss rate is measured before any state or USDA recovery, so moving a loss onto another public program can't make the bank look better. Guarantee fees and any payouts go into [the deal's ledger](#who-buyouts-are-compared-against) and the money test. And each deal's file shows the total public money and risk in it, across every program, not just the bank's.
|
||||
- **It's all published.** Each year the bank reports how many deals used a state or USDA program, how many the state turned down and why, and how many the bank stepped back from. Where Treasury's and USDA's deal records allow, co-op buyouts they supported elsewhere are reported next to the bank's own.
|
||||
- **It asks for these fixes too.** Alongside the SBA change, the model asks Treasury to encourage states to open their programs to worker buyouts, and USDA to raise its $600,000 cap for conversions. Where they do, the track shrinks, as it would for the SBA fix.
|
||||
|
||||
These programs show that public help for worker buyouts already exists, state by state and in pieces. What the track can show is whether one lender, with the same rules everywhere, reaches good deals the patchwork doesn't.
|
||||
|
||||
### What the help costs
|
||||
|
||||
An earlier version of this page set aside $5 million for help. Counted properly, that was too little.
|
||||
|
||||
The best public benchmark is the ESOP sale. The National Center for Employee Ownership says setting one up costs "a low end of $200,000 to $500,000 for most deals," and that an outside trustee adds costs "in the mid-five figures per year" ([NCEO](https://www.nceo.org/employee-ownership-faq/how-much-does-setting-up-an-esop-cost)). The sales here are smaller than most ESOP deals, around $2.5 million, and a co-op doesn't need an ESOP's yearly trustee or yearly valuation. So this model's figures sit below NCEO's. They're its own estimates, not NCEO's:
|
||||
|
||||
- **The appraisal**, $15,000 to $35,000. NCEO puts a first ESOP appraisal in the "low five figures and up."
|
||||
- **The profit check**, the outside accountant's restatement of profits, $10,000 to $30,000. No public price was found for firms this small, so this is a guess.
|
||||
- **The workers' adviser**, $30,000 to $60,000 for the whole sale. That's less than NCEO's figure for a trustee, because the adviser's job is one sale, not years of legal duty as a fiduciary.
|
||||
- **Help after the sale.** $15,000 of standard help for every firm, and about $70,000 of intensive help for the half who win the lottery ([The Pilot, in Numbers](pilot-spec.md#the-support-lottery)). That's about $50,000 a firm on average.
|
||||
- **Deals that fall through.** Some owners will get an appraisal and walk away, or the workers will vote no. Nobody knows how many. This model assumes one and a half to two deals get as far as an appraisal for every one that closes.
|
||||
|
||||
Who pays for what:
|
||||
|
||||
- **The seller pays for the checks.** Before the appraisal, the seller puts down a deposit that covers it and the profit check, a fixed amount set in advance that doesn't depend on the value found. It's kept at closing, since these are costs of selling. If the seller walks away, it's kept too, so an owner can't get a free valuation to use in some other sale. It's paid back only if the workers vote no or the bank turns the deal down. If the seller then sells the business to someone else within two years, they repay it. Nothing is added to the workers' loan, so the debt stays within the appraised value.
|
||||
- **The public pays for the rest:** the workers' adviser, the checks on deals that fall through for reasons that aren't the seller's doing, the re-appraisals in [one deal in ten](#a-fair-price), and the help after the sale. Deposits that are kept go back into the help budget.
|
||||
- **If money runs short, fewer deals, not thinner help.** Each deal's help is set aside when its loan is made, the same rule [the pilot](pilot-spec.md#the-support-lottery) uses.
|
||||
|
||||
On those numbers, the public pays roughly $110,000 to $240,000 for each sale that closes. So $12 million is set aside. That covers the 55 to 60 sales the loan money allows unless costs run near the top of the range, where it covers only about 50. In that case the help budget, not the loan money, sets the number of deals, and any loan money left unlent goes back to Treasury.
|
||||
|
||||
Because help is given to only half the firms by lottery, the results can show something about what it does. With about 30 firms in each group, that's only a rough signal, and it's reported alongside the startup lottery, not as a test of its own.
|
||||
|
||||
**The money test counts all of it, with no exceptions.** Help is a cost the track has to earn back, just as it is in [the startup test](pilot-spec.md#the-support-lottery), and that makes the test hard to pass. A rough guide: loans go out over the first few years and are paid back over ten, so by year twelve each dollar lent has been outstanding for about five years on average. At 2 to 3 points more than the Treasury rate, $88 million lent that way earns something like $9 million to $14 million more than Treasury would have. Out of that come losses, $12 million of help and the track's share of running the bank. So the track passes only if its losses are low and its help costs sit near the bottom of the range. That's said now, before the first loan, so a failure can't be explained away afterward.
|
||||
|
||||
### How many deals
|
||||
|
||||
After the $12 million for help, $88 million at about $1.5 million a deal, half the limit, would fund roughly 55 to 60 buyouts, and the help budget may hold it nearer 50. With a private lender and the seller's share on top, that typical deal is a business priced around $2.5 million. That's too few for the tight survival comparison the startup test uses. So survival is published and compared, but as evidence, not as a pass-or-fail line. The terms of every deal are published: the price, the appraisals, the loans and the seller's share. So is what happened to them: every cut to a seller's share, every year payments had to wait, and the losses on deals the bank made alone, reported apart from the rest from the start.
|
||||
|
||||
### Who buyouts are compared against
|
||||
|
||||
Three groups of businesses can be followed. None of them makes a clean experiment, so the comparisons describe; they don't prove that worker ownership caused a difference. The reviewer publishes the matching rules, the code and every classification before the first buyout loan, so nobody can pick the comparison after seeing the results.
|
||||
|
||||
**1. Businesses bought with government-backed small-business loans.** The SBA publishes every loan in its main program, including the borrower's industry, the loan size and term, the approval year, and whether the loan was paid off or written off. It also marks loans made for a "change of ownership" ([SBA loan data and its field list](https://data.sba.gov/dataset/7a-504-foia)). By this site's count of the SBA file as of June 2026, leaving out cancelled loans, there were about 4,600 to 6,900 a year from 2020 to 2025, and about 13,800 of $1 million or more since 2020.
|
||||
|
||||
- **How they're matched.** On industry, approval year, the whole deal's debt (not just the bank's slice) and loan term. A long term usually means the loan bought a building too, and in these buyouts the seller often keeps it. Loans that look like partner buyouts or employee-ownership deals are dropped where they can be spotted.
|
||||
- **What's compared.** The share of money written off, at the same loan age on both sides. The SBA reports write-offs before anything is recovered afterward, so the bank's losses are counted the same way.
|
||||
- **The loss rate the test uses.** For change-of-ownership loans of $1 million to $5 million approved in 2015 to 2019, about 1.9% of the money lent, and 3.1% of the loans, had been written off by mid-2026. That's a floor, not a final figure. About 39% of those loans were still being repaid, and the SBA publishes nothing about them beyond that, not even whether they're behind. And in 2020 and 2021 the government made 6 to 14 months of payments on these loans for their borrowers ([15 U.S.C. 9011](https://www.law.cornell.edu/uscode/text/15/9011)), which kept some from failing. So the bank's rate comes from a formula the reviewer fixes in advance: the SBA write-off rate in dollars for the same industries, sizes and terms, at the same loan age, adjusted for the pandemic years in a way that's also published. The reviewer's job is to make sure it wasn't set high, since a high rate is the easy one to beat.
|
||||
- **Chance.** About 65 loans at a loss rate like that means two or three failures. One more or one fewer moves the rate a lot. So the published rate comes with a margin worked out in advance for how much it could miss by chance alone, and the test fails only if losses land above that margin. That means the test can catch a track that does much worse than SBA lending, but it can't show that buyouts do better. The report says so.
|
||||
- **The deals aren't built alike.** The bank's loans come after three audited years of profits, with a checked margin over the payments and a seller who waits to be paid behind the bank. SBA loans often have neither. Lower losses may just show that the screen and the seller's share work, not that worker ownership does, and the report says that too.
|
||||
|
||||
**2. Businesses sold to an employee stock ownership plan (ESOP).** An ESOP is a retirement plan that owns company stock for the workers. Each such plan files a yearly return with the Labor Department that marks it as an ESOP, and separately as one that borrowed to buy the stock, gives the company's tax number and industry, and counts the plan's members ([Form 5500 instructions](https://www.dol.gov/sites/dolgov/files/ebsa/employers-and-advisers/plan-administration-and-compliance/reporting-and-filing/form-5500/2025-instructions.pdf)). A plan that ends files a final return.
|
||||
|
||||
- **How they're matched.** By industry, state and size, starting from the first year a plan is marked as having borrowed to buy the stock, which is the closest the filings come to the date of the sale. ESOPs are expensive to set up and rare in companies this small, so the reviewer first reports how many matches exist. If there are too few, this comparison is dropped rather than stretched.
|
||||
- **What's compared.** Whether the firm is still open after 5 and 10 years, and whether it's still owned by its workers. A plan's member count includes some people who've left, so it's only a rough measure of jobs.
|
||||
- **Sales.** A final return doesn't say why the plan ended. The company may have closed, been sold, or merged the plan into another. Coders who don't know which firm is which sort each case using state business records, and every call is published. For survival, a sale counts as neither success nor failure, and then both ways, as in the startup test. For worker ownership, a sale counts as losing it, since that's the usual way ESOP ownership ends and exactly what the lock prevents.
|
||||
|
||||
**3. Deals that almost happened.** Owners who applied but whose deals fell through: on the profit test, the payment margin, the appraisal or the workers' vote. They're the closest thing to the same businesses without a buyout. The reviewer follows them through the same records. Where a deal missed a line narrowly, like a vote decided by a few workers, it's close to a coin flip, and the reviewer reports those cases separately.
|
||||
|
||||
**Pay and jobs.** Neither public record has pay. The track's pay test uses the co-ops' own payroll and linked government wage records ([The Pilot](pilot-spec.md#what-gets-measured)). The bank applies for access to those records before the first buyout loan. If access is refused, the report says so instead of leaving the comparison out quietly. Pay growth is also compared with pay in the same industry and county from public wage statistics.
|
||||
|
||||
**Where the money went.** For each deal, and for the track as a whole, the report adds up 12 years of payments: to the seller, including everything counted as part of the price, interest to the private lender and to the bank, and what members got above their pay before the sale, in wages, profit shares and account balances. Passing the pay test only shows that workers weren't made worse off while they paid for the business. The ledger shows how much they gained, and how much went to the seller and the lenders. The seller's line includes any capital gains tax they put off ([above](#the-tax-break-that-already-exists)).
|
||||
|
||||
**What none of this covers.** Most small businesses change hands with no SBA loan and no ESOP, through cash, private loans and seller financing, and those sales leave no public trail. So the comparisons show how buyouts do against two kinds of financed sale and against deals that fell through, not against every sale.
|
||||
|
||||
**Expect opposition.** Business brokers and private equity funds, who make money when owners sell, may see a public lender as a competitor. Italy's main employers' association got part of its worker-buyout program suspended by the EU ([Has This Been Tried?](precedents.md#italys-co-op-funds-coopfond-and-the-marcora-law)). Keeping the bank at market rates with no new tax break is the best defense, because it leaves little to call unfair. Critics can still point to the old break, which is why its cost counts against the track. The rest is in [Making It Last](durability.md).
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **The workers pay for what they helped build.** A fair price is a price for the owner's years of ownership, and the workers pay it out of profits their own work will produce. This model accepts that, because the alternative is taking the business, and it doesn't take anyone's business. The rules keep the price fair. They can't make it free.
|
||||
- **These loans have less behind them than ordinary ones.** Almost no cash from the buyers, no personal guarantees and a seller who waits two years, not ten. The coverage test and the seller's paid-last share carry the weight that cash and guarantees carry in an SBA loan. The cushion is also thinner than in past co-op buyouts: the main loan here is about 80% of the price, where the conversions Project Equity reviewed had 50–70% in main loans, with other lenders as well as the seller paid after them. If that isn't enough, it will show up as losses above the published rate, and the track fails its test.
|
||||
- **An old tax break rides along.** Sellers who qualify can put off, and often never pay, capital gains tax. The model counts its cost against the track but doesn't stop it. If many sellers take it, the track will probably fail its money test.
|
||||
- **The seller always knows more.** Written disclosures, checked profits and a shrinking seller's share make hiding problems costly. They can't make it impossible.
|
||||
- **A fair price is a matter of judgment.** Appraisals are estimates. The rules make overpaying harder and put the seller's own money behind the price, but they can't make valuation exact.
|
||||
- **The vote can be pressured.** A secret ballot, a neutral ballot and a majority of everyone eligible help. They can't stop a well-liked owner or manager from leaning on people in the break room.
|
||||
- **One failure can become a story.** A single buyout that goes wrong will make a better headline than sixty that don't. Publishing the expected loss rate in advance, and every deal's terms, lets anyone check whether a failure was bad luck or bad lending. It doesn't stop the headline.
|
||||
- **"Forever" is a lot to ask of a business that wasn't founded that way.** Some owners won't sell under the lock, and some critics will say it's wrong to lock a firm its members never chose to lock at founding. Whether the lock should protect the co-op form or only its value is an open question for every co-op, not just these ([Open Problems](open-problems.md)).
|
||||
- **It may pull good founders away from co-ops.** A founder who expects to sell to their workers one day can start a conventional company, keep full control for 20 years, and then sell at a fair price. That may be a better deal than founding a co-op ([The Founder Problem](founder-problem.md)). For the model's goal, more workplaces owned by the people who work there, it's still a win. But it could leave fewer strong founders for the startup test.
|
||||
- **Sixty buyouts is a small sample.** The track shows whether the loans can be repaid safely without cutting pay. It can't show much more than that about whether the workers who were bought in end up better off.
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
title: Member Capital Accounts
|
||||
section: The Co-op
|
||||
order: 25
|
||||
summary: How a co-op member builds real savings from the firm's profits without the firm ever being sold, how those savings are protected, and what they add up to.
|
||||
---
|
||||
|
||||
# Member Capital Accounts
|
||||
|
||||
In a conventional company, you build wealth from the business by owning shares and eventually selling them. In a locked co-op, nobody can sell the firm (see [The Lock](the-lock.md)). So there has to be another way for the value you help build to become *yours*. That's what a member capital account is for.
|
||||
|
||||
The idea isn't new. The Mondragon co-ops in Spain have used it since the 1950s, and US worker co-ops use it under ordinary federal tax law. This page describes how it works in this model, and how members' savings are protected. Protection matters, because savings held inside your own employer are exactly the kind that can vanish along with your job.
|
||||
|
||||
## The basic idea
|
||||
|
||||
Every member has a personal account on the co-op's books. It works like a savings account the business holds for you:
|
||||
|
||||
- **Each year, part of your share of the profits goes into it** instead of being paid out as cash.
|
||||
- **It earns interest.**
|
||||
- **It's paid back to you** on a fixed schedule: each year's credit about five years after it's made, whether or not you still work there.
|
||||
|
||||
Meanwhile, the co-op gets to use that money to grow. In effect it's a loan from the members to their own business. That's why Mondragon valued it: a firm that can't sell shares to outsiders can still keep its own earnings working ([Morris, *The Mondragón System*, 1992](https://www.ilsr.org/wp-content/uploads/2013/03/mondragon-3.pdf)).
|
||||
|
||||
## Where each year's profit goes
|
||||
|
||||
When the co-op makes a profit, it gets split. These are the defaults. Each co-op can change them in its bylaws, as long as it stays within the minimums.
|
||||
|
||||
| Share of profit | Where it goes | Why |
|
||||
| --- | --- | --- |
|
||||
| **At least 30%** | The co-op's permanent reserve | Nobody can ever withdraw it (see [indivisible reserves](the-lock.md)). It's the firm's cushion and its own source of investment money. |
|
||||
| **Up to 10%** | The federation's shared fund, if the co-op belongs to one | Pools risk between co-ops (see [The Federation](federation.md)). |
|
||||
| **The rest, about 60%** | Everyone who worked that year, divided by hours worked | This is the "profit share" from [Worker Ownership](worker-ownership.md). It ramps up with tenure, then caps. The part of a share someone hasn't reached yet goes to the reserve ([How a Locked Co-op Is Taxed](taxes.md)). |
|
||||
|
||||
Mondragon's original split was similar: 10% to education and the community, at least 20% to reserves, and up to 70% to members. It later raised the reserve minimum to 50% (Morris, 1992).
|
||||
|
||||
The members' share is then split again, between cash now and the account:
|
||||
|
||||
- **At least 20% of it is paid in cash right away.** Members owe income tax on their whole allocation in the year it's made, including the part that goes into the account. The cash is there so they can pay that tax. This is how the usual kind of co-op credit works under US tax law; a second kind, taxed only when paid out, is in [How a Locked Co-op Is Taxed](taxes.md#capital-accounts) ([Democracy at Work Institute guide](https://institute.coop/sites/default/files/resources/Internal-Capital-Accounts.pdf)). The default is half in cash.
|
||||
- **Lower-income members can take it all in cash.** For a low-wage worker, being taxed on money they can't touch can cost them tax credits and health-care subsidies. So any member whose household income is under twice the poverty line can take their whole share in cash, or the standard split, with nothing in between, so nobody chooses how much to invest. They choose before the year starts, and one low year doesn't qualify. All cash is the default, and like everyone they're signed up for a retirement plan outside the co-op ([below](#saving-for-retirement)). Once the Labor Department has answered on pension law, those who take the standard split won't be taxed on the account part until it's paid out ([How a Locked Co-op Is Taxed](taxes.md#capital-accounts)).
|
||||
- **The rest goes into each member's account.**
|
||||
|
||||
Members can also raise their own salaries instead of taking profit, but that doesn't get around the reserve. It takes its share of pay above the going rate too (see [Investing for the Long Run](the-long-run.md#the-rules)).
|
||||
|
||||
**A worked example.** A 40-person co-op makes $600,000 in profit after paying everyone's salaries. It puts $180,000 in its permanent reserve and $60,000 in the federation fund. The remaining $360,000 is divided by hours worked. If all 40 members work the same hours, each one's hours earn $9,000. A member at full share gets it all: $4,500 as a check and $4,500 added to their account. A member at half a share gets $4,500, and the other $4,500 goes to the reserve. Profit earned by the hours of workers who aren't members yet is shared the same way, paid in cash, at the share of a member just starting out. Federal tax law is why it works this way ([How a Locked Co-op Is Taxed](taxes.md)).
|
||||
|
||||
## How the account grows
|
||||
|
||||
- **Interest is set by formula, not by vote.** A current member's account earns the one-year Treasury rate plus two percentage points, every year. It's credited after the co-op's tax on it, since the co-op can't deduct it ([How a Locked Co-op Is Taxed](taxes.md#interest-on-accounts)). Once a member leaves, their balance earns the plain Treasury rate. So does the slice of a current member's account that's protected by [insurance](savings-guarantee.md), since it's no longer at risk. If current members voted on the rate, they could set it to zero on accounts that mostly belong to people who've left. Or senior members could set it high to pay themselves out of the firm. A formula stops both.
|
||||
- **Why the extra two points.** Money in a current member's account is at risk: it shares in losses and ranks behind lenders. If it only earned what a risk-free government bond pays, members would vote to pay out as much in cash as the rules allow, and the co-op would have less of its own money to invest. Once you leave, your balance stops sharing in losses, so it stops earning the premium. See [Investing for the Long Run](the-long-run.md#the-rules).
|
||||
- **Losses are shared.** In a year the co-op loses money, the loss is charged partly to the reserve and partly to members' accounts, in the same proportions as profits. Your account can go down, just like shares in any company.
|
||||
|
||||
## How it's paid out
|
||||
|
||||
- **Each year's credit is paid back after five years.** The money credited to your account for 2030 is paid to you in 2035, the 2031 credit in 2036, and so on, oldest first, each after that year's accounts are closed and checked by an outside accountant ([What the Rules Cost a Small Co-op](compliance.md#what-a-review-can-and-cant-stand-in-for)). Each co-op can set the wait anywhere from three to seven years in its bylaws. It's one of the two ways US worker co-ops already pay out accounts, called a "roll-over" ([Democracy at Work Institute guide](https://institute.coop/sites/default/files/resources/Internal-Capital-Accounts.pdf)). The co-op gets to use each year's credit for years, and nobody's savings are stuck until they retire.
|
||||
- **The wait is fixed when the credit is made.** A vote to change it applies only to credits made starting the year after the vote, so senior members can't speed up their own money or slow down anyone else's. For credits that aren't taxed until paid, it applies only to profit from years that start after the vote ([How a Locked Co-op Is Taxed](taxes.md#credits-taxed-later-the-deferred-pay-rules)).
|
||||
- **Leaving doesn't change the schedule.** When you leave, your balance is fixed at the end of that year, after that year's profit or loss is counted, so nobody can quit just ahead of a bad year and escape it. After that it stops sharing in losses, earns the plain Treasury rate, and each year's credit is still paid on its own date.
|
||||
- **Small balances are paid early, for everyone.** Once anyone's whole balance is under $2,000, member or not, it's paid at the next payout date. That keeps a co-op from tracking small sums for years, and since it doesn't depend on leaving, nobody gains by quitting.
|
||||
- **There's no penalty for leaving.** Mondragon used to cut the payout by up to 30% for people who quit. This model doesn't, because a core promise of [the tenure cap](worker-ownership.md) is that you can change jobs without losing what you've built.
|
||||
- **Accounts are capped at one year's pay,** measured as the highest pay in the last three years, so nobody can cut their own pay to get their money sooner. Anything above that is paid out every year in cash, which a member can put in their own retirement account and invest in anything. Nobody should end up with most of their savings tied to the same company that pays their wages.
|
||||
- **Any payout can be spread out.** Members get 90 days' notice of each payout and can take it at once or in up to 12 monthly payments, whichever works better with the public benefits they get ([How a Locked Co-op Is Taxed](taxes.md#benefits)). For credits that aren't taxed until paid, that choice is made before the year the credit is earned, and the payments all fall in one calendar year ([How a Locked Co-op Is Taxed](taxes.md#credits-taxed-later-the-deferred-pay-rules)).
|
||||
- **On death, the account goes to the person you name,** your spouse unless you name someone else with their written consent, on the normal schedule.
|
||||
- **In a divorce, a court can split the account.** The former spouse gets a separate account of their own, with no vote, paid out on the normal schedule.
|
||||
- **Every year, each person gets a statement** showing each credit, any losses charged to it, and the date it will be paid. Former members get one too. A dispute gets a written answer within 90 days and can go to an outside arbitrator from the federation's list, or to court.
|
||||
|
||||
### Why not pay it out when you leave?
|
||||
|
||||
Mondragon pays members their accounts when they retire or leave, and so do some US co-ops. This model doesn't, because of a federal pension law. Under the Employee Retirement Income Security Act (ERISA), any program an employer runs that holds back workers' income "for periods extending to the termination of covered employment or beyond" is a pension plan ([29 U.S.C. 1002(2)(A)](https://www.law.cornell.edu/uscode/text/29/1002)). The Labor Department's rule says the same about bonuses "systematically deferred to the termination of covered employment" ([29 CFR 2510.3-2(c)](https://www.law.cornell.edu/cfr/text/29/2510.3-2)). Members are the co-op's employees ([Owners Are Still Workers](labor-law.md)). The model treats profit allocations as an owner's share, not an employer's bonus, but it doesn't stake members' savings on winning that argument. In states where wage law could reach account credits, co-ops don't use accounts at all until the law changes ([Owners Are Still Workers](labor-law.md#is-the-profit-share-pay)).
|
||||
|
||||
A pension plan would break the account. Its money has to be held in a trust, apart from the business ([29 U.S.C. 1103](https://www.law.cornell.edu/uscode/text/29/1103)), and the plan can't lend to the employer ([29 U.S.C. 1106](https://www.law.cornell.edu/uscode/text/29/1106)). The whole point of the account is that the co-op uses the money.
|
||||
|
||||
**A fixed schedule is designed to stay outside the law's definition, but that isn't settled.** In 1995 the Labor Department said a company's profit-based program wasn't a pension plan because it didn't "expressly provide for a deferral of income to the termination of covered employment." But that program paid nothing to people who quit, and the Department pointed to that. It also warned that a program can still become a pension plan "as a result of surrounding circumstances," such as "an inordinate percentage" of participants being near retirement age when payments arrive ([Advisory Opinion 95-23A](https://www.dol.gov/sites/dolgov/files/EBSA/about-ebsa/our-activities/resource-center/advisory-opinions/erisa-section-3-3-employee-pension-benefit-plan-advisory-opinion-1995-23a.pdf)). Here, people who leave are still paid afterward, on the same dates as everyone else. A court could count that against the design.
|
||||
|
||||
**Courts weigh the facts differently.** Two federal appeals courts have found pension plans in programs that called themselves deferred compensation, let employees choose to put off part of their pay, and paid it when they left. The Fifth Circuit did in 2014 ([*Tolbert v. RBC Capital Markets*, 758 F.3d 619](https://www.courtlistener.com/opinion/2683548/brenda-tolbert-v-rbc-capital-markets-corp/)) and the Sixth in 2019 ([*Wilson v. Safelite*, 930 F.3d 429](http://www.opn.ca6.uscourts.gov/opinions.pdf/19a0148p-06.pdf)). Both read "results in a deferral" literally. But the Fifth Circuit's own earlier rule still stands: "the mere fact that some payments under a plan may be made after an employee has retired or left the company does not result in ERISA coverage" (*Murphy v. Inexco Oil*, 1980, quoted in *Tolbert*). In April 2026 the Fourth Circuit held that awards paid eight years after they were granted were a bonus program, not a pension plan, partly because 92% of the payments went to people still working there ([*Milligan v. Merrill Lynch*](https://www.govinfo.gov/content/pkg/USCOURTS-ca4-25-01385/pdf/USCOURTS-ca4-25-01385-1.pdf)). It listed six things courts can weigh:
|
||||
|
||||
1. whether everyone takes part, or only some
|
||||
2. whether it's paid for with money workers would otherwise get now
|
||||
3. whether money is really set aside
|
||||
4. whether workers can choose to put payment off until they leave
|
||||
5. whether it's presented as retirement savings
|
||||
6. whether the firm's results change what's paid
|
||||
|
||||
**The model's argument, and its fallback.** First, nothing is put off until anyone leaves. Each credit is paid on a date fixed when it's made, the same for everyone, whether or not they've left. Members are taxed on it that year as if they'd received it, which counts against any "deferral of income." And it's members' capital in their own co-op, not an employer's plan for holding back pay. If a court treats the credit as pay anyway, the fallback is the Labor Department's rule: a share of profit on top of wages is a bonus for work, and it isn't systematically put off until people leave. That fallback concedes, for pension law only, that the share is a bonus. That sits uneasily with the model's position under tax and wage law that it's an owner's share, not pay ([Owners Are Still Workers](labor-law.md#is-the-profit-share-pay)).
|
||||
|
||||
**Two things weaken it.** One is the lower-income option. Credits that aren't taxed until paid, chosen by the member in advance and run under the tax rules for deferred pay, look most like the plans in *Tolbert* and *Wilson*. The other is that nobody loses their account for leaving, so a lot of it is paid to people who've left. If a tenth of members leave each year, about 40% of each five-year credit belongs to someone who's gone by the time it's paid, and about 27% with a three-year wait. It's less by dollars, since people who leave tend to be newer, with smaller credits. In home care, where even co-ops lose about 38% a year ([When People Don't Stay Long](high-turnover.md)), it's most of it. So:
|
||||
|
||||
- **Nothing about the payout depends on leaving:** not the date, not the amount, not the small-balance rule.
|
||||
- **The wait is short and the same for everyone,** at most seven years, not tied to age or retirement.
|
||||
- **Credits taxed later wait for the Labor Department.** Until the Department answers, co-ops don't offer them. Lower-income members choose between all cash, the default, and the standard split, taxed now ([How a Locked Co-op Is Taxed](taxes.md#credits-taxed-later-the-deferred-pay-rules)).
|
||||
- **Most of what's paid goes to people still working there.** The federation's software reports each year what share of a co-op's account payouts, in dollars, went to people who weren't working there, counting small balances and payments to heirs. It also reports the share paid to people at or near retirement age. If more than a third went to people who weren't working there, averaged over three years, the co-op shortens the wait for new credits to three years. If it's still over a third after that, it stops making new credits, and the members' share is paid in cash. A third is the model's own number, far looser than the 8% in *Milligan*, so the request to the Department asks where the line is. Stopping new credits doesn't change the status of old ones, which are still paid on schedule.
|
||||
- **The account isn't written or sold as deferred pay or retirement savings.** The bylaws describe it as members' capital under state co-op law. Retirement saving happens the ordinary way, in a separate plan held outside the business ([below](#saving-for-retirement)).
|
||||
- **The same rules apply everywhere.** Under ERISA a suit can be brought where the plan is run, where the breach took place, or wherever a defendant can be found ([29 U.S.C. 1132(e)(2)](https://www.law.cornell.edu/uscode/text/29/1132)). A former member who moves can take the case with them, so a rule based on where the co-op sits wouldn't protect it.
|
||||
- **The first pilot co-ops ask the Labor Department for an advisory opinion on their real bylaws, and the bank pays.** An opinion only covers the co-op that asked, on its own facts, so the bank also asks the Department for a rule any employer could use, for profit-sharing paid on a short fixed schedule. The Fourth Circuit held that the Department acted within its power to define the law's terms when it wrote its bonus rule ([29 U.S.C. 1135](https://www.law.cornell.edu/uscode/text/29/1135)). Other circuits don't have to agree. It doesn't ask Congress for an exemption only co-ops would get.
|
||||
|
||||
**The cost is capital.** Money paid back after five years is money the co-op can't keep using for fifteen. So a co-op that wants to invest more has to keep more in its permanent reserve, and that weakens the model's answer to the horizon problem ([Investing for the Long Run](the-long-run.md#who-pays-for-an-investment-and-when)).
|
||||
|
||||
### Saving for retirement
|
||||
|
||||
Every co-op also offers an ordinary retirement plan, held outside the business, to everyone who works there, members or not. It's a normal benefit, not a replacement for the account. But it matters most for people who don't build savings in the co-op: members of co-ops that have stopped making credits, and lower-income members who take their share in cash. They're mostly the lowest-paid.
|
||||
|
||||
- **A plan the federation doesn't run.** Employers that share "a common interest other than having adopted the plan" can't form their own pooled plan, and co-ops in one federation probably do ([29 U.S.C. 1002(43)](https://www.law.cornell.edu/uscode/text/29/1002)). So each co-op joins an open pooled 401(k) run by a provider registered with the Labor Department, which takes on most of the paperwork and legal duties. The federation compares providers and recommends one. No part of the federation, the bank or its software company is paid from plan money, and fees are a flat amount per person, published every year.
|
||||
- **Everyone is signed up, as the law requires.** A 401(k) plan started since 2023 has to sign up every eligible worker at 3% to 10% of pay, raised by a point a year to at least 10%, unless the worker chooses otherwise. For the first 90 days they can take their own contributions back ([26 U.S.C. 414A](https://www.law.cornell.edu/uscode/text/26/414A)). Co-ops under three years old or with ten or fewer workers are exempt but use the same rules. The default starts at 3%, and changing it or opting out takes one step. When one large company started signing new workers up automatically, 86% took part, against 37% before ([When People Don't Stay Long](high-turnover.md)).
|
||||
- **The co-op puts in 3% for everyone, not a match.** A match would come out of shared profit and go only to people who can afford to save, so members who opt out because they need the cash would pay for those who don't. Instead the co-op adds 3% of wages for every eligible worker, whether they save or not, and it's theirs at once. That's one of the law's standard "safe harbor" designs ([26 U.S.C. 401(k)(12)(C)](https://www.law.cornell.edu/uscode/text/26/401)). It also spares the co-op the yearly tests comparing what owners and other workers get, which matters because in a co-op with fewer than 20 members, each member holds more than 5% of the vote, and tax law may treat each one as an owner.
|
||||
- **The federal match.** From 2027, the Saver's Match adds 50% of up to $2,000 a year that a lower-income person saves for retirement, paid by the Treasury into a non-Roth account. It shrinks as income rises and is gone at $35,500 for a single filer, $53,250 for a head of household and $71,000 for a married couple, before inflation adjustments ([26 U.S.C. 6433](https://www.law.cornell.edu/uscode/text/26/6433)). It's paid the year after, only to people who file a tax return and claim it, so the co-op's [benefits check](taxes.md#benefits) includes help filing. Money taken out within about two years reduces it. The default is the ordinary pre-tax option. Many low-paid workers owe almost no income tax, so after-tax Roth savings would often suit them better, but the Treasury hasn't said yet whether Roth savings earn the match.
|
||||
- **No withdrawals or loans while working there.** SSI doesn't count a retirement fund as savings if the worker has to leave the job to get any of it ([SSA POMS SI 01120.210](https://secure.ssa.gov/poms.nsf/lnx/0501120210)). Hardship withdrawals or loans would probably make it count. So the plan allows neither, beyond the 90-day refund the law requires. Once someone leaves, their balance can be cashed out, so it counts for SSI then, and cashing out early costs a 10% tax penalty. A member on SSI can stop their own savings, though the co-op's 3% still goes in. Balances roll over directly to a new plan or retirement account unless the worker chooses otherwise.
|
||||
|
||||
An example: a home care aide who heads a household and earns $30,000 saves 3%, or $900. The co-op adds $900, and the Treasury $450 the next year, so $2,250 goes into an account that nobody can take from her if the co-op fails. She's just under the income where the Treasury's match starts to shrink, so a raise or other household income would reduce it. The co-op's 3% is a cost, paid before any profit is shared, and in thin-margin work that's real money. But it counts as pay under the Medicaid rule for home care ([When People Don't Stay Long](high-turnover.md#when-medicaid-decides-how-much-goes-to-pay)), and it's within the 6% of pay that [the pay rule](the-long-run.md#the-rules) leaves alone.
|
||||
|
||||
### Are accounts securities?
|
||||
|
||||
Securities law covers far more than stock. Its definition includes any "note," any "evidence of indebtedness," and any "participation in any profit-sharing agreement" ([15 U.S.C. 77b(a)(1)](https://www.law.cornell.edu/uscode/text/15/77b)). An account credit is a promise to pay a set amount, with interest, on a set date. If credits counted as securities, a co-op would need an exemption from registering them, and anything misleading it told members about them could be grounds for a lawsuit.
|
||||
|
||||
**The case that they aren't:**
|
||||
|
||||
- **Members don't buy them.** Credits come out of each year's profit share, by one split the bylaws set for everyone. The Supreme Court held that a pension plan workers didn't pay into wasn't a security, because an employee "is selling his labor primarily to obtain a livelihood, not making an investment" ([*Teamsters v. Daniel*, 1979](https://www.law.cornell.edu/supremecourt/text/439/551)).
|
||||
- **Membership isn't like stock.** The Court held that shares in a housing co-op weren't securities. They paid no dividends from profit, couldn't be sold or pledged, gave no extra votes for extra shares, and couldn't rise in value ([*United Housing Foundation v. Forman*, 1975](https://www.law.cornell.edu/supremecourt/text/421/837)). Membership here is the same: one vote per member, and a buy-in of at most $1,000 held apart from the account at its face value, earning nothing ([When People Don't Stay Long](high-turnover.md#joining-costs-nothing-up-front)). The profit share comes from hours worked, not from the membership.
|
||||
|
||||
**The case that they might be:**
|
||||
|
||||
- **Notes are presumed to be securities.** In 1990 the Court held that a farm co-op's notes to its members and others were securities. It looked at why the co-op issued them and why people held them, how widely they were offered, what the public would expect, and whether anything else protected the holders. It stressed that the co-op used the money for its business, and that the notes were "uncollateralized and uninsured" and would otherwise "escape federal regulation entirely" ([*Reves v. Ernst & Young*](https://www.law.cornell.edu/supremecourt/text/494/56)). An account is also an unsecured promise from the co-op, the co-op uses the money, and the extra two points of interest are there to make keeping it in worthwhile. The [savings insurance pool](savings-guarantee.md) covers only part of it.
|
||||
- **This model gives up the protection *Daniel* relied on.** The Court said workers didn't need securities law there because pension law already protected them. Accounts here are designed to stay outside pension law.
|
||||
- **A former member is simply owed money** by a company they no longer work for, and a payout paused for two years becomes a new debt to them (below).
|
||||
|
||||
**So the model keeps accounts on the safe side of each test:**
|
||||
|
||||
- **Nobody can put money in.** Accounts are filled only from profit shares. No member can choose to leave more in. Lower-income members can take everything in cash or the standard split, nothing in between.
|
||||
- **Accounts can't be sold, pledged or borrowed against** (below), and they aren't offered as an investment. A co-op can't advertise account interest to recruit workers, and statements describe accounts as "partly covered by a private insurance pool with no government backing," not as insured savings.
|
||||
- **Members get the facts.** Every account holder, former members included, gets the yearly statement and the co-op's checked accounts. Anything misleading in them could be grounds for a claim either way, so the federation writes the model statements and buys directors' liability insurance for member co-ops as a group. That's a cost [What the Rules Cost a Small Co-op](compliance.md) doesn't count yet.
|
||||
- **The bank asks the SEC for a general answer.** The first pilot co-ops ask the SEC's staff to confirm they won't treat credits as securities, and the bank pays. A staff letter binds no court, no state and no member who sues, so the bank also asks the SEC for a rule that covers profit allocations paid on a short fixed schedule, as it does with the Labor Department.
|
||||
- **State law too.** Each state has its own securities law. The bank's checked entry for each state ([Owners Are Still Workers](labor-law.md#is-the-profit-share-pay)) covers it, alongside wage law. California, for example, exempts "credits to a member's capital" in co-ops formed under its co-op law, for voting members, and doesn't count patronage dividends toward its $1,000 limit on what a member puts in ([Cal. Corp. Code § 25100(r)](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=CORP§ionNum=25100)). That shows how a state can handle credits. It doesn't settle federal law, and it doesn't cover former members, who no longer vote.
|
||||
|
||||
**The fallback has a price.** If credits did count as securities, the nearest federal exemption covers securities a company issues to its employees under a written compensation plan, up to the largest of $1 million, 15% of its assets, or 15% of that kind of security outstanding, each year ([17 CFR 230.701](https://www.law.cornell.edu/cfr/text/17/230.701)). But using it means calling credits pay, in writing. The tax, pension and wage-law positions elsewhere all rest on credits being an owner's share, not pay ([Owners Are Still Workers](labor-law.md#is-the-profit-share-pay)), and the IRS or a state wage agency could point to that document. It also doesn't cover a paused payout that becomes a new debt to a former member, who isn't an employee anymore. So the model doesn't plan on it. It's a last resort, and the conflict is listed in [Open Problems](open-problems.md).
|
||||
|
||||
## Protecting the co-op, and the people who stay
|
||||
|
||||
A payout system can also be used to strip a failing firm. People who see trouble coming leave first and get paid in full, while everyone who stays absorbs the loss. So:
|
||||
|
||||
- **Every payout must pass a solvency test.** No payment may push the co-op below its loan requirements or a minimum cash level set in its bylaws when it's founded. Members can't lower that minimum without the bank's consent.
|
||||
- **If payouts fail the test, they're paused, all together, for at most two years.** Credits from the same year are treated the same whether their owner still works there or not, and the oldest are paid first when money allows. Paused amounts earn the Treasury rate plus two points, since the people waiting are carrying extra risk. While any payout is paused, **current members get no cash profit share, no bonuses and no raises above the going rate.** Nobody gets paid ahead of the people who are waiting.
|
||||
- **Credits that aren't taxed until paid follow stricter rules.** Lower-income members' nonqualified credits are paused only if an accountant finds that paying them would put the co-op at risk, not just for the loan terms or the cash minimum. They're paid as soon as it wouldn't, even ahead of the note below. That's an exception to treating the same year's credits the same, because federal rules on deferred pay require it ([How a Locked Co-op Is Taxed](taxes.md#credits-taxed-later-the-deferred-pay-rules)).
|
||||
- **After two years, a pause becomes a debt with a schedule.** Whatever is still unpaid becomes a note the co-op repays in equal parts over five years, still behind its lenders, and the [insurance pool](savings-guarantee.md) covers former members' insured amounts if the co-op fails. So the worst case for a member who leaves is a long wait: up to seven years for the last credit, plus the pause, plus the note. The co-op's bylaws have to say so in plain words.
|
||||
- **Losses are spread evenly.** When a loss is charged to members' accounts, it's charged to every year's credits that haven't been paid yet, in proportion, not to the newest or the oldest first. So the timing of payouts can't be used to dodge a loss.
|
||||
- **Payouts rank behind the co-op's lenders.** In a bankruptcy, members' accounts are paid only after the co-op's creditors. That's the honest cost of being an owner, and it's the main reason for the one-year-of-pay cap. The tax relief on a lost account is slow, too: members were taxed on the credits in full, but can probably deduct only $3,000 a year of the loss, unless Congress passes the fix the model asks for ([How a Locked Co-op Is Taxed](taxes.md#if-the-co-op-fails)).
|
||||
- **Accounts can't be cashed in early, sold, pledged or borrowed against,** from the co-op or anyone else. That would just be an early withdrawal by another name. It also keeps the account from counting against public benefits' savings limits before it's paid ([How a Locked Co-op Is Taxed](taxes.md#benefits)). The only transfers are to a named person on death and a court's division in a divorce.
|
||||
- **Savings are insured, up to a point.** Every locked co-op belongs to a members' savings insurance pool, paid for by the co-ops. It covers workers' final unpaid wages, former members' unpaid payouts up to $50,000, and a protected slice of current members' accounts. See [Protecting Members' Savings](savings-guarantee.md).
|
||||
- **No other savings in the co-op.** A co-op can't take deposits from its members or sell them bonds. A member's only money in the co-op is their account.
|
||||
|
||||
**Why this much caution.** Fagor, Mondragon's appliance maker and once the world's largest industrial co-op, went bankrupt in 2013 with about €800 million (about $1.1 billion) of debt ([The Local, 2013](https://www.thelocal.es/20131114/major-manufacturer-fagor-files-for-bankruptcy)), putting 5,600 jobs at risk. Savers who had bought its subordinated co-op bonds, which had been sold as safe deposits, ended up suing ([Wikipedia summary of the Mondragon record](https://en.wikipedia.org/wiki/Mondragon_Corporation)). Shortly before Enron collapsed in 2001, about 63% of its workers' 401(k) retirement savings were in Enron's own stock ([GAO-02-480T](https://www.gao.gov/assets/gao-02-480t.pdf)). It's the same lesson: savings concentrated in the company you work for are the savings most likely to disappear with your job.
|
||||
|
||||
## What the account doesn't do
|
||||
|
||||
This matters, because it's the core of [the founder problem](founder-problem.md).
|
||||
|
||||
**The account holds the profits you earned. It doesn't hold the value of the firm's future profits.** When a conventional firm is sold, the buyer pays for years of its future earnings, and the seller keeps that money. A co-op member's account never includes that part. The future profits go to whoever works there in the future.
|
||||
|
||||
That's intentional. It's what the lock is for. But it means a co-op member who helped build something valuable walks away with much less than a conventional owner would, and the model shouldn't pretend otherwise.
|
||||
|
||||
## What it adds up to
|
||||
|
||||
Here's what a member who joined a successful co-op in its third year has received by year 15, on top of their salary, in four kinds of business. The numbers come from a simple model with these assumptions:
|
||||
|
||||
- A three-month probation, the standard bylaws' default ([When People Don't Stay Long](high-turnover.md#a-short-probation)), paid at the starting share. Then a profit share that grows from 20% to 100% over five years.
|
||||
- 60% of profit goes to members, minus the founding share in the first ten years ([The Founder Problem](founder-problem.md#the-tools)). This member started in year three, so they get a small part of it too. The part of a share a newer member doesn't get yet goes to the reserve, not to other members, as tax law requires ([How a Locked Co-op Is Taxed](taxes.md#the-tenure-ramp)).
|
||||
- The workforce grows steadily to the size shown, as on the next page.
|
||||
- Money received later counts for less than money received now. It's discounted at 5% a year, the rate used in the founder study on the [next page](founder-problem.md).
|
||||
|
||||
| Kind of business | Profit per worker (after salaries) | Size at year 15 | **What the member received** |
|
||||
| --- | --- | --- | --- |
|
||||
| Care work (home care, cleaning) | $3,000 | 40 people | about **$12,000** |
|
||||
| Main Street (trades, services) | $15,000 | 40 people | about **$60,000** |
|
||||
| Strong (skilled services, light manufacturing) | $40,000 | 80 people | about **$160,000** |
|
||||
| Very high margin (a firm like Conservice) | $150,000 | 200 people | about **$600,000** |
|
||||
|
||||
In a conventional firm, all of that goes to the owners.
|
||||
|
||||
**Be honest about the first row.** Home care runs on thin margins. For a care worker, a co-op offers a real say in how the work is done, and very little wealth. That's why the model has a separate answer for low-margin work: [the floor](the-floor.md).
|
||||
|
||||
These are all success cases. A co-op that struggles pays little or nothing. One that fails pays out only what's left after its debts.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: Capital, After
|
||||
section: Growing the Sector
|
||||
order: 80
|
||||
summary: Outside investment in a co-op is rented, not owned: profit-sharing claims that expire and carry no vote.
|
||||
---
|
||||
|
||||
# Capital, After
|
||||
|
||||
Outside investment into a locked co-op doesn't disappear. It changes shape.
|
||||
|
||||
**Rented, not enthroned.** An investor funds the company and receives repayment plus a share of profits for the life of the loan. Then the claim **extinguishes**, and the workers hold 100%.
|
||||
|
||||
Today's equity is *perpetual*: an original investor's great-grandchildren collect forever off work they never did. This claim *expires*. That's the whole reform, in one word. Capital gets rented.
|
||||
|
||||
The returns can be enormous. A profit-share loan into a risky seed-stage co-op might pay 10x or 30x. That's fine. The principle was never *no big payday*. It was **no perpetual claim and no control**.
|
||||
|
||||
There is one limit, though. A claim that takes 90% of the profits for 40 years is "voteless and expiring" on paper, but it turns the co-op into a shell. So for co-ops borrowing from the bank, outside claims rank behind the bank's loan, last at most 15 years, and need the bank's approval. See [Guarding the Pilot](pilot-safeguards.md#draining-it-through-investors).
|
||||
|
||||
**Be honest about what this is.** A profit-contingent return means the investor eats the loss when things go badly. That is residual risk, which is the economic job we call equity, whatever we print on the paper. Don't claim it "isn't a stake in the work." It is. Claim what's actually true and actually better: *finite, and voteless.*
|
||||
|
||||
**The consequence, priced in.** Capital charges for risk. An instrument with equity-like downside and a terminating upside gets funded last, or demands a bigger profit share. Revenue-based financing already exists in today's market and stays niche for exactly this reason. It bites hardest on brand-new ventures and barely at all on mature, profitable firms.
|
||||
|
||||
**Investors are buying securities.** A share of profits sold to an investor is a "participation in any profit-sharing agreement," which federal law lists as a security ([15 U.S.C. 77b(a)(1)](https://www.law.cornell.edu/uscode/text/15/77b)). A co-op raising outside money has to register the offering or fit an exemption, and it answers for anything misleading it tells investors. The usual routes for a small business:
|
||||
|
||||
- **Crowdfunding:** up to $5 million a year through a registered online portal. Most investors have a yearly limit. If either their income or their net worth is under $124,000, it's the greater of $2,500 or 5% of whichever of the two is larger ([17 CFR 227.100](https://www.law.cornell.edu/cfr/text/17/227.100)).
|
||||
- **Selling locally:** only to residents of the co-op's own state, under that state's rules ([17 CFR 230.147A](https://www.law.cornell.edu/cfr/text/17/230.147A)), or up to $10 million a year under each state's own rules where it's sold ([17 CFR 230.504](https://www.law.cornell.edu/cfr/text/17/230.504)). Both leave the co-op to state review.
|
||||
- **Wealthier and experienced investors:** any amount, to investors who meet the federal wealth or income test, plus up to 35 others who are experienced in business ([17 CFR 230.506](https://www.law.cornell.edu/cfr/text/17/230.506)). States can't require these offerings to be registered, so it's often the cheapest route, though not one for a co-op's neighbors.
|
||||
- **One large investor:** a parent co-op funding a [spin-off](growth.md), or a lender joining the bank, can invest privately without a public offering ([15 U.S.C. 77d(a)(2)](https://www.law.cornell.edu/uscode/text/15/77d)).
|
||||
|
||||
Each route costs legal and filing fees, which weigh most on small raises. The federation keeps standard offering documents for these claims, with a standard warning about the risks: the claim can't be sold easily, it ranks behind the bank, and it can be worth nothing. So each co-op doesn't pay a lawyer to write its own. The co-op's directors, often members who earn an ordinary wage, answer for anything misleading in an offering, so the federation's group liability insurance covers them. A co-op that just wants to borrow can also take an ordinary loan with fixed payments, which courts usually don't treat as a security. Members' own [capital accounts](capital-accounts.md#are-accounts-securities) are a different case.
|
||||
|
||||
This rule applies only to co-ops. Conventional firms keep conventional equity. That makes the cost-of-capital question an empirical one: [the test](the-test.md) will show whether co-ops can raise enough on these terms, alongside the bank, to grow.
|
||||
@@ -0,0 +1,154 @@
|
||||
---
|
||||
title: What the Rules Cost a Small Co-op
|
||||
section: The Co-op
|
||||
order: 32
|
||||
summary: The model's checks, from audits to the lock-holder's reviews, are scaled to a co-op's size and done once a year in one package, with audits by lot to deter cheating, though even the lighter package costs a small co-op real money.
|
||||
---
|
||||
|
||||
# What the Rules Cost a Small Co-op
|
||||
|
||||
Every page of this model adds a check. Someone has to confirm that members were paid the going rate, that the job titles are honest, that the equipment isn't wearing out unreplaced, that nobody is being pushed out before they'd join. Each check makes sense on its own. Together, they could cost a small co-op more than it earns.
|
||||
|
||||
That matters for two reasons. Rules a small firm can't afford end up existing only on paper, or they quietly keep co-ops out of small, low-margin work, which is where [the model most wants them](who-gets-loans.md). This page says what's checked, by whom, how often, and who pays.
|
||||
|
||||
## What gets checked
|
||||
|
||||
Across the model, a locked co-op is asked to do these things every year:
|
||||
|
||||
| Check | Where it comes from |
|
||||
| --- | --- |
|
||||
| Year-end accounts, closed and checked by an outside accountant | [Capital account payouts](capital-accounts.md#how-its-paid-out), [savings cover pricing](savings-guarantee.md) |
|
||||
| Every worker paid at least the going rate for their job, by occupation code | [The pay rule](the-long-run.md#the-rules), [Is the profit share pay?](labor-law.md#is-the-profit-share-pay) |
|
||||
| A sample of timesheets matched to payroll records | [What counts as an hour](taxes.md#what-counts-as-an-hour) |
|
||||
| Direct care job titles checked, where Medicaid's 80% rule applies | [When Medicaid decides how much goes to pay](high-turnover.md#how-a-co-op-meets-it) |
|
||||
| Overtime recalculated once the profit share is set | [Overtime and the profit share](labor-law.md#overtime-and-the-profit-share) |
|
||||
| Pay ratio and pay bands, by job, gender and race | [Who Sets the Pay](pay.md#everyone-sees-the-bands) |
|
||||
| Warnings, hearings and dismissals, plus an anonymous workload survey | [When a member won't do the work](free-riding.md#when-a-member-wont-do-the-work) |
|
||||
| Hires, and people let go before they could join | [No pushing people out](high-turnover.md#no-pushing-people-out-before-theyd-join) |
|
||||
| Spending on buildings and equipment next to how much they wore out | [Running it down](the-long-run.md#running-it-down) |
|
||||
| Big expansion proposals and how members voted | [When Growing Means Sharing](growth.md) |
|
||||
| A statement to each member of their account | [Member Capital Accounts](capital-accounts.md) |
|
||||
| Contracts with insiders, priced like deals with strangers | [The hidden boss](pilot-safeguards.md#the-hidden-boss) |
|
||||
| Members' share of all work hours, counting agency and contractor hours | [The hidden boss](pilot-safeguards.md#the-hidden-boss) |
|
||||
|
||||
Most of this is data a co-op already has in its payroll and bookkeeping. The expensive part is having an outside accountant check it.
|
||||
|
||||
## What outside checking costs
|
||||
|
||||
Outside accountants offer three levels of checking. A **compilation** just puts the books into proper form, with no checking at all. A **review** asks questions and looks for numbers that don't make sense, without testing the records underneath. An **audit** tests the records and gives a formal opinion. There's also a fourth kind of job, **agreed-upon procedures**, where the accountant runs a fixed list of tests and reports what they found, without giving an opinion on the accounts as a whole. It's billed on top of a review, not inside it.
|
||||
|
||||
For very small firms, the gap between a review and an audit is large. When the SEC wrote its rules for small companies raising money online, it estimated a review at about $1,500 to $18,000 and an audit at about $2,500 to $30,000. The average audit for companies with under $1 million in both market value and revenue was about $20,000 in 2014 ([SEC, 2015](https://www.federalregister.gov/documents/2015/11/16/2015-28220/crowdfunding)). By 2020, commenters and industry sources the SEC cited put a review of a small company at roughly $1,500 to $6,000, and one said at least $6,000 for a company with two years of history ([SEC, 2021](https://www.federalregister.gov/documents/2021/01/14/2020-24749/facilitating-capital-formation-and-expanding-investment-opportunities-by-improving-access-to-capital)).
|
||||
|
||||
Take a cleaning co-op with six members and $400,000 in sales. A yearly audit at $20,000 could be most of its profit. The model's own rough estimate for the package below is more like this, per year:
|
||||
|
||||
| Item | Rough cost |
|
||||
| --- | --- |
|
||||
| Review of the accounts | $3,000 to $6,000 |
|
||||
| The fixed list of extra tests | $1,500 to $3,000 (the model's guess; no good public data) |
|
||||
| Lock-holder's review, every two years | $500 to $1,000 a year (the model's guess) |
|
||||
| Staff time | 20 to 40 hours |
|
||||
|
||||
That's roughly $5,000 to $10,000, or 1.25% to 2.5% of sales. In cleaning, where profit might be 5% of sales, that's a quarter to half of the profit. The package is cheaper than yearly audits, but it isn't cheap, and the page doesn't pretend otherwise.
|
||||
|
||||
## How others do it
|
||||
|
||||
**Italy** has checked every co-op for decades. The law requires a "cooperative revision" at least once every two years, or every year where a special law says so. If the co-op belongs to one of the national federations, the federation does it; if not, the government does. The revision has two jobs: to advise the board on running the co-op and on internal democracy, and to confirm the co-op is real. It checks that the members are real, that they take part, and that the co-op qualifies for its tax breaks, and it checks that worker-members are treated as the co-op's own rules say. A co-op that fails can be struck off the co-op register, put under a government-appointed manager, or dissolved ([Legislative Decree 220/2002, articles 2, 4 and 12](https://www.parlamento.it/parlam/leggi/deleghe/02220dl.htm)).
|
||||
|
||||
It's paid for by a fee on a published scale. For 2025–26, the smallest co-ops pay €330 for the two years, but only if they have at most 100 members, at most €5,160 in share capital *and* at most €75,000 in sales. Going over any one of those puts a co-op in a higher band. The top band, for co-ops with over €2 million in sales, is €2,740 ([Ministry decree of 12 February 2025, as reported by Fisco7](https://www.fisco7.it/2025/06/contributo-revisione-cooperative-2025-2026-in-vigore-i-nuovi-importi/)).
|
||||
|
||||
**Italy is also a warning.** Cheap checking by the co-ops' own federations has lived alongside a long-running problem of "false co-ops," firms that use the co-op form to underpay workers. In the first three months of 2016, labor inspectors who went looking for them inspected 934 co-ops and found more than half breaking the rules, with 3,768 workers affected ([Italian Ministry of Labour, 2016](https://www.lavoro.gov.it/stampa-e-media/comunicati/pagine/ministero-del-lavoro-vigilanza-sulle-cooperative-spurie)). Those were targeted inspections, not a random sample, so they don't show how common false co-ops are. They do show that a federation checking its own members isn't enough on its own.
|
||||
|
||||
**Mondragon's bank** asked every co-op for monthly financial reports, and gave each a full audit every four years ([Has This Been Tried?](precedents.md)).
|
||||
|
||||
## The rules
|
||||
|
||||
### Checking is scaled to size
|
||||
|
||||
Three sizes. Size is measured by sales and by full-time-equivalent workers, counting hours worked through agencies and contractors, so taking on contractors or part-timers doesn't keep a co-op small. Co-ops that are related, in the sense [the pilot already uses](pilot-safeguards.md#splitting-one-borrower-into-many) (a shared founder, manager, landlord, franchisor or major supplier), are counted together, so splitting a firm in two doesn't either. A co-op moves up a size when it passes either line two years running.
|
||||
|
||||
- **Small: under $2 million in sales and under 25 full-time equivalents.** A yearly review by an outside accountant, plus the fixed list of extra tests (below). No scheduled audit.
|
||||
- **Middle: $2 million to $10 million, or 25 to 100 full-time equivalents, or any co-op owing the bank more than $750,000.** A yearly review with the extra tests, and a full audit every other year.
|
||||
- **Large: over $10 million, or over 100 full-time equivalents.** A full audit every year.
|
||||
|
||||
Every co-op also sends the bank its quarterly payroll tax filings, which it already has, so the bank sees hours and pay four times a year at almost no cost.
|
||||
|
||||
### What a review can and can't stand in for
|
||||
|
||||
Wherever the model says "the co-op's auditor," it means whichever outside accountant does that year's review or audit. But a review isn't an audit, and a few rules need the real thing:
|
||||
|
||||
- **Capital account payouts** can go ahead on reviewed accounts. If a later audit finds profit was overstated, the extra credits are taken back out of members' unpaid credits, the same way [losses are shared](capital-accounts.md#how-its-paid-out).
|
||||
- **Savings cover** starts only after the pool itself audits the co-op, at the pool's cost. After that, premiums can be priced on reviewed accounts, and the pool can audit any covered co-op at any time ([Protecting Members' Savings](savings-guarantee.md)).
|
||||
- **Spreading the cost of a big project**, under [the long-run rule](the-long-run.md#the-rules), still needs an independent audit. A review doesn't count. A small co-op that wants to use the rule has to pay for one.
|
||||
|
||||
### One package, done once a year
|
||||
|
||||
- **The extra tests are the same everywhere.** The accountant runs a fixed list published by the bank and the federations before the first loan:
|
||||
- every contract with an insider (a founder, manager, member's family, or a company they own), checked against what strangers would pay, the main way [a hidden boss](pilot-safeguards.md#the-hidden-boss) gets paid;
|
||||
- members' share of all work hours, checked against payroll *and* against agency and contractor invoices;
|
||||
- every worker's pay against the going rate for their occupation code, and, in co-ops under 25 full-time equivalents, every worker's code, with a sample in larger ones;
|
||||
- a sample of timesheets against payroll;
|
||||
- job titles where the Medicaid rule applies;
|
||||
- the overtime recalculation.
|
||||
- **One problem widens the check.** If a sample turns up anyone underpaid, the test is widened to every worker. Everyone underpaid, member or not, gets the difference with interest, and the co-op gets a full audit.
|
||||
- **The list can't quietly grow.** Adding a test to the package requires publishing what it will cost co-ops, and the whole package's cost is reported every year.
|
||||
- **The data comes from payroll.** Pay bands, the pay ratio, hires and departures, and members' statements come out of the standard payroll service the model already uses for overtime ([Owners Are Still Workers](labor-law.md#overtime-and-the-profit-share)). The co-op doesn't build them by hand.
|
||||
- **One report.** Every yearly report on the list above goes on one standard form, filed once, with the lock-holder, the bank and the members. None of the model's own bodies asks a co-op for the same number twice. Government agencies like Medicaid or the IRS will still ask for their own.
|
||||
- **Small groups are protected from the public, not from the members.** Where a published figure would reveal one person's pay or discipline, the public sees only the total. Members still see everything [the pay rule](pay.md#everyone-sees-the-bands) gives them, including managers' actual pay. The accountant and the lock-holder see the figures person by person, and a gap in pay or discipline between groups is a flag for the lock-holder's review.
|
||||
- **The federation does the templates.** Standard bookkeeping, the report form, the occupation codes and the benchmark for the going rate are supplied centrally, so a small co-op isn't paying an accountant to invent them. Clean books also make the review cheaper.
|
||||
|
||||
### The accountant isn't the co-op's to pick freely
|
||||
|
||||
- **From an approved list.** The co-op chooses its accountant from a list the bank approves, and changes firms at least every seven years.
|
||||
- **The lock-holder picks the samples**, so the co-op can't steer which timesheets or pay records get tested.
|
||||
- **The report goes straight to the lock-holder and the bank**, as well as to the members.
|
||||
- **Changing accountants is a warning sign.** If an accountant resigns or is replaced, the outgoing one writes to the bank saying why, and the co-op gets a full audit.
|
||||
|
||||
### Audits by lot, and when something looks wrong
|
||||
|
||||
Scaling checks down leaves more room to cheat in small co-ops. Three things close part of that gap:
|
||||
|
||||
- **One in twenty, by lot, after year-end.** Each year, after the books close, the pilot's outside reviewers draw one small co-op in twenty for a full audit, by a method published in advance, the same way they run [the support lottery](pilot-spec.md#the-support-lottery). At least half the draw is purely random. The rest is weighted toward co-ops with warning flags: heavy use of agency or contractor hours, a lot of insider contracts, or high turnover. The auditor is assigned, not chosen. What the audits find is published every year. Once a co-op has repaid the bank, the lock-holders run a narrower draw instead, of insider deals only ([Deals with insiders](the-lock.md#deals-with-insiders)).
|
||||
- **Warning signs trigger an audit.** A small co-op gets a full audit if:
|
||||
- its accounts are late, or its accountant resigns, is replaced or qualifies its report;
|
||||
- the extra tests find a problem;
|
||||
- it loses money two years running;
|
||||
- equipment spending stays below wear long enough to call in [the inspector](the-long-run.md#running-it-down);
|
||||
- payments to insiders grow faster than sales;
|
||||
- its payroll tax filings don't match its accounts;
|
||||
- members' share of work hours falls toward the minimum, or many hires are let go before they could join;
|
||||
- it ends a year below Medicaid's 80% pay ratio, where [that rule applies](high-turnover.md#when-medicaid-decides-how-much-goes-to-pay);
|
||||
- a labor or anti-discrimination agency makes a finding against it;
|
||||
- it falls behind on the bank's loan;
|
||||
- or before any sale of a large part of the business under [the lock](the-lock.md#closing-the-back-doors).
|
||||
- **Anyone who works there can complain.** Members, non-members and anyone who left in the last two years can complain anonymously, in their own language, to the lock-holder or straight to the bank. They're protected by the same rule that protects members who report problems ([When a member won't do the work](free-riding.md#when-a-member-wont-do-the-work)). An upheld complaint leads to a targeted check of what it's about, not a full audit, so it can't be used just to run up a co-op's costs. Either side can appeal the lock-holder's decision to a different approved lock-holder.
|
||||
|
||||
The bank pays for audits by lot and for triggered audits, as part of its cost of lending, counted in its results like any lender's monitoring. When an audit finds fraud, the co-op pays it back.
|
||||
|
||||
### The lock-holder's review, every two years
|
||||
|
||||
- **Like Italy's revision, with more workers in it.** At least once every two years, the lock-holder checks that the members are real and do most of the work, that the membership rules and the insider-deal rules are followed, and that working conditions and discipline follow the co-op's own rules. It talks privately with a random group of workers, members and non-members, including people who left in the last two years, with an interpreter where needed. Its report goes to the members' meeting and the bank, not just the board.
|
||||
- **Advice is separate and optional.** A co-op can buy advice from its lock-holder or federation, but the person who checks a co-op can't be its paid adviser in the same years.
|
||||
- **A fee set from the real cost.** Lock-holders charge on a scale set by size, but the scale is approved by the bank from what a real review costs, not set by the lock-holders themselves. A fee too low to pay for a real review would just buy a weak one. Where the federation holds the lock, the fee is part of its dues and isn't charged twice.
|
||||
- **Switching lock-holders is allowed, but not to escape a check.** A co-op can move to another approved lock-holder by a members' vote, but not while an audit, an unresolved finding or a vote the lock-holder can veto is open, and not in the 12 months before a review is due. The old lock-holder's file goes to the new one, and the review clock doesn't restart.
|
||||
|
||||
### What happens when a co-op fails a check
|
||||
|
||||
In steps, and each step can be appealed. A finding comes with a deadline to fix it. If it isn't fixed, payouts to board members and managers stop, new savings cover is suspended, and the bank can treat it as a breach of the loan. A co-op that refuses to be reviewed or audited is in breach of the loan and of the lock, and the lock-holder can go to court. None of this gives the lock-holder power to run the co-op.
|
||||
|
||||
### Who pays at the start
|
||||
|
||||
The bank's first loan can include the first three years of checking costs as working capital, so a new co-op doesn't have to find them in its first thin years. The pilot's business-help budget pays only for the shared pieces: the templates, the report form and the payroll service's setup.
|
||||
|
||||
### The cost is measured, and it can move the rules
|
||||
|
||||
Before the first loan, the bank and the federations publish what they expect the whole package to cost a co-op of each size. The pilot then records what it actually costs each co-op, as a share of its sales and of its profit, and publishes it by size and industry, along with what the audits by lot find.
|
||||
|
||||
The goal is under 1% of sales for small co-ops. The estimate above already misses it. If the typical small co-op spends more than 1% of sales for two years running, *and* the audits by lot are finding little, the lightest-value checks are cut back in an order published in advance: the group pay figures to every other year first, then the timesheet sample, then the review itself to every other year, with a compilation in between. If the audits are finding a lot, the checks stay, and the report says what honest checking costs in low-margin work.
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **Less checking means more room to cheat.** A reviewed small co-op is checked less deeply than an audited one. Audits by lot, warning signs and complaints make cheating risky, not impossible. Italy's false co-ops are the warning.
|
||||
- **It costs real money in the work that can least afford it.** At 1% to 2.5% of sales, the package could take a large part of a cleaning or home care co-op's profit. The model has no way to make honest checking free, only cheaper.
|
||||
- **The cost numbers are rough.** Two of the four lines in the estimate are guesses. There's no good US data on what small firms spend on outside accounting as a share of sales. The pilot's own numbers will be the first.
|
||||
- **Checkers can be captured.** The co-op still pays its accountant, and lock-holders depend on the co-ops they check. The approved list, rotation, samples picked by the lock-holder, draws by outside reviewers and the limits on switching are meant to limit that. Italy has the same tension.
|
||||
- **Fees don't cover staff time.** Filling in a form and answering an accountant's questions takes hours in a firm with no office staff. Payroll data and templates cut that down. They don't remove it.
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: What If Conservice Had Been a Co-op?
|
||||
section: Making the Case
|
||||
order: 160
|
||||
summary: A real company that has taken on new private equity owners three times in ten years, imagined instead as a locked co-op from the day it was founded, with who would govern it, where its profit would go, and who would have received less.
|
||||
---
|
||||
|
||||
# What If Conservice Had Been a Co-op?
|
||||
|
||||
[The thesis](thesis.md) uses one example: "TPG's stake in Conservice." This page works that example through.
|
||||
|
||||
It isn't a plan to take Conservice from anyone. This model doesn't buy or seize companies, and nothing here says any owner or founder did anything wrong. Every sale described below was lawful and chosen by the people selling. The question is about the structure, not the people: **what if a company with Conservice's business had been founded as a [locked co-op](the-lock.md) in 2000?** Who would govern it, where would its profit go, and who would have received less?
|
||||
|
||||
Everything on this page comes from public sources.
|
||||
|
||||
## The company as it is
|
||||
|
||||
Conservice, based in Logan, Utah, was founded on August 28, 2000, by David Jenkins and a business partner. It started out billing apartment residents for their utilities ([Herald Journal, 2020](https://www.hjnews.com/news/business/progressive-workplace-conservice-marks-20-years-as-standout-local-employer/article_5768eef4-b633-58a2-b036-7077c76cf86e.html)). Today it handles utilities for nearly 8 million rental units and other properties, working with more than 20,000 utility providers ([TPG, 2025](https://www.tpg.com/news-and-insights/tpg-to-acquire-majority-stake-in-conservice)).
|
||||
|
||||
It had 2,083 employees in 2020, up almost 900 in four years. By 2025 more than 2,000 came from its home county alone, making it one of the county's largest private employers. About 5,000 local people have worked there over its 25 years ([Herald Journal, 2025](https://www.hjnews.com/news/business/logan-based-company-conservice-celebrates-25-years/article_c24ef502-2cb3-4c43-916d-9a7320ec5977.html)). Its total headcount across all offices isn't public.
|
||||
|
||||
A local-paper headline called it a "standout local employer." It moved its staff home early in the pandemic when, as its founder said, "we could make people come in the office." It gives paid time for community service and matches part of workers' retirement savings ([Conservice careers page](https://www.conservice.com/careers)). Those benefits are real. They're also the owners' to give and the owners' to take back. **This page isn't about bad management.** It's about who holds the claim on everyone's work.
|
||||
|
||||
**Who owns it.** Private equity firms, which buy into companies with investors' money and borrowed money, have held stakes in it one after another:
|
||||
|
||||
- **2016:** TA Associates invests ([TA](https://www.ta.com/portfolio/investments/conservice/)).
|
||||
- **2020:** Advent International buys a minority stake, at a value of $2.66 billion for the whole company ([Herald Journal, 2020](https://www.hjnews.com/news/business/progressive-workplace-conservice-marks-20-years-as-standout-local-employer/article_5768eef4-b633-58a2-b036-7077c76cf86e.html)).
|
||||
- **December 2025:** TPG agrees to buy a majority stake. TA sells out completely, and Advent keeps a large stake. The price wasn't published ([TPG](https://www.tpg.com/news-and-insights/tpg-to-acquire-majority-stake-in-conservice)).
|
||||
|
||||
**What it earns.** TPG's purchase was backed by a loan package of more than $1 billion. The company was presented to lenders as earning about $230 million a year before interest, taxes and the accounting charges for wear and tear on its assets (a measure called EBITDA). People close to the deal told the trade press the real figure was more likely $175 million to $225 million ([Octus, January 2026](https://octus.com/resources/articles/update-1-blue-owl-crescent-expected-to-lead-private-credit-financing-for-conservice-buyout-by-tpg/)).
|
||||
|
||||
## Where the money goes now
|
||||
|
||||
- **Workers** get salaries and benefits. As far as public sources show, they have no vote on who owns the company, who runs it or where its profit goes.
|
||||
- **Lenders** get the interest on more than $1 billion. That money backed TPG's purchase. In deals like this it usually also pays off the company's earlier loans. Either way, it's the company's own earnings that pay it back.
|
||||
- **Owners** get what's left, and then the gain when they sell to the next owner. Each sale paid the previous owners, including the founders, at a price the buyer put at risk. And each price was a bet on the future work of people who still work there and had no say in the sale.
|
||||
|
||||
That's what the thesis means by a claim on other people's labor that the owner doesn't work alongside. A stake like that can be sold, again and again, and each sale puts a price on thousands of people's future work.
|
||||
|
||||
## The same company, founded as a co-op
|
||||
|
||||
Now imagine the same business, the same customers and the same growth, but founded in 2000 as a worker co-op under this model. That's a big assumption, discussed below.
|
||||
|
||||
**Who governs.** The workers are members, each with one vote. They elect a board, and the board hires the chief executive. Managers still manage. Members don't vote on every decision, only on what their bylaws say they vote on ([Worker Ownership](worker-ownership.md)). The company could have leaders much like its current ones. The difference is who they answer to.
|
||||
|
||||
**Not everyone is equal on day one.** New workers become members after a probation of up to a year. Their share of the profit then grows over about five years until it's full. Non-members can do up to a quarter of the work hours, but no more ([Guarding the Pilot](pilot-safeguards.md#the-hidden-boss)).
|
||||
|
||||
**No sales.** Under [the lock](the-lock.md), the company can never be sold to conventional owners. The 2016, 2020 and 2025 deals don't happen. No buyer means no billion-dollar loan to pay back.
|
||||
|
||||
**Where the profit goes.** Under the [default split](capital-accounts.md#where-each-years-profit-goes), 30% goes to a permanent reserve that nobody can ever withdraw, which pays for growth, the job outside investors' money does today. Up to 10% goes to the [federation](federation.md)'s shared fund. About 60% goes to members, split by how long each has worked there, up to a cap. At least a fifth of each member's share is paid in cash, half by default. The rest goes into their [capital account](capital-accounts.md), which is paid out after they leave. So nearly everyone among the 5,000 people who have worked there, everyone who stayed past probation, would have left with something.
|
||||
|
||||
**Unions still have a place.** Members can still organize, and a union can still bargain over pay, schedules and grievances with the managers the board hires. Owning the company doesn't make every disagreement disappear.
|
||||
|
||||
## What it could mean in dollars
|
||||
|
||||
This model's case isn't about money ([Making the Case](the-argument.md)). But the numbers show the scale.
|
||||
|
||||
They need two assumptions, because the real figures aren't public:
|
||||
|
||||
- **Profit.** EBITDA is higher than real profit, because it leaves out the cost of equipment and software that wears out. So assume real profit of **$125 million to $175 million** a year.
|
||||
- **Headcount.** More than 2,000 employees come from the home county, and there are other offices. So assume **2,500 to 4,000** workers.
|
||||
|
||||
The table also assumes the average member holds about 70% of a full share, since newer members are still ramping up.
|
||||
|
||||
| Real profit | Workers | **A full-share member's profit share each year** (before income tax; about half in cash, the rest in their account) |
|
||||
| --- | --- | --- |
|
||||
| $125 million | 4,000 | about $27,000 |
|
||||
| $125 million | 2,500 | about $43,000 |
|
||||
| $175 million | 4,000 | about $38,000 |
|
||||
| $175 million | 2,500 | about $60,000 |
|
||||
|
||||
In a co-op, salary and profit share are two parts of one income. Members set the split, within the model's rules. The figures above are on top of whatever salaries the co-op pays, which might not match today's. On taxes: a co-op generally doesn't pay corporate income tax on the profit it assigns to members, but members pay income tax on their whole share in the year it's assigned. The reserve is taxed like any company's profit.
|
||||
|
||||
These are rough numbers from public reporting, for a company that may never have existed in this form. They show the scale, not a promise.
|
||||
|
||||
## Would it have grown?
|
||||
|
||||
This is the honest weak point.
|
||||
|
||||
- **It grew for sixteen years before any private equity firm bought in.** By 2016 it already had roughly 1,200 employees (2,083 in 2020, less the almost 900 added in the four years before). So its early growth didn't depend on private equity.
|
||||
- **Private equity's years brought acquisitions.** In 2020 it bought Capturis, a North Dakota company that pays commercial buildings' utility bills ([Conservice, 2020](https://www.conservice.com/press-releases/conservice-acquires-capturis-bringing-value-to-commercial-markets-served/)), and it has bought others since. A co-op can buy companies too. But under [the lock](the-lock.md), the people who work there have to become members, and some sellers wouldn't accept that. Otherwise the co-op ends up like Mondragon's foreign subsidiaries: a members' club that owns firms full of workers with no stake. So a co-op version would probably be smaller, and some of today's earnings came from companies it might not have bought.
|
||||
- **Borrowing would be harder.** A first loan from [the bank](the-bank.md), at market rates with no personal guarantees, is exactly what the bank exists for. (The real company had two founders. The [pilot](pilot-spec.md#who-can-borrow) would have required a third.) But the pilot caps each co-op at $3 million in loans over its lifetime, so the bank gets a company like this started, not grown. After that, a profitable business can borrow from ordinary lenders. Conservice's earlier loan came from private lending funds at a normal rate for a company like it ([Octus](https://octus.com/resources/articles/update-1-blue-owl-crescent-expected-to-lead-private-credit-financing-for-conservice-buyout-by-tpg/)). But those lenders were lending to a company with owners' money beneath them to absorb losses first. A co-op, with its members' accounts paid out as people leave and [the lock](the-lock.md) narrowing who could buy it if it failed, would likely borrow less, at a higher price.
|
||||
|
||||
Whether co-ops can grow on those terms is exactly what [the test](the-test.md) is for.
|
||||
|
||||
## Who would have received less
|
||||
|
||||
**The founders.** As members, they would have had a founding share, their accounts, and executive pay while they ran it. They would never have sold. In the model's 200-person example, a co-op founder ends up with about $980,000 against $29 million to $38 million for a conventional owner ([The Founder Problem](founder-problem.md#the-numbers)). Conservice was valued at $2.66 billion in 2020, so at its scale the gap would be far wider. What the founders actually received hasn't been published.
|
||||
|
||||
The model's aim is for that value to stay with the people who keep producing it. But that raises the question the whole model rests on: **would these founders have started this company as a co-op?** Nobody knows. The founder problem isn't solved, and this example doesn't solve it.
|
||||
|
||||
**The investors.** TA, Advent and TPG could still have lent to the co-op, or put in money for a share of profits that ends after a set time ([Capital, After](capital.md)). That would still be a claim on the members' work. The model's answer isn't that such a claim is free. It's that the claim ends and carries no vote. What they couldn't have bought is a permanent claim, the kind that can be sold again.
|
||||
|
||||
**The workers, in one way.** A member's account rises and falls with the company. The [insurance pool](savings-guarantee.md) protects part of it, not all of it.
|
||||
|
||||
## What this example doesn't prove
|
||||
|
||||
- **It assumes the company would exist either way.** That's the biggest assumption on the page. If worker ownership makes firms less productive, or if the founders would never have started it, the comparison falls apart. [The pilot](pilot-spec.md) is built to find out.
|
||||
- **Conservice is unusually profitable.** A home-care co-op has very little profit to share. For low-margin work, ownership brings a say and not much money, which is why the model also has [a floor](the-floor.md).
|
||||
- **Ownership changes who gets the profit, not who pays it.** A co-op version would bill the same renters, who don't choose their building's billing company. They'd still need the same protection from unfair fees that they need now.
|
||||
- **Dollars aren't the main point.** It's about who decides. Thousands of people in one valley have seen new owners buy into their company three times in ten years, and none of them had a vote on any of those deals. In the co-op version, they would.
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: The Constitution
|
||||
section: Guardrails
|
||||
order: 130
|
||||
summary: The biggest risk is that the model gets repealed, not that the state overreaches. Plus money out of politics and rights the majority can't take away.
|
||||
---
|
||||
|
||||
# The Constitution
|
||||
|
||||
This model asks little of the state: charter [a bank](the-bank.md), enforce [the lock](the-lock.md), run [the floor](the-floor.md). The concentration-of-power argument still applies to the floor (see [Open Problems](open-problems.md) on "essential" sectors), but it isn't the biggest risk.
|
||||
|
||||
**The main risk is durability, not overreach.** A model that can be built gradually can be dismantled gradually. A future Congress can defund the bank, and a statutory lock can be repealed by the same kind of majority that passed it. Postwar social democracy got rolled back once the coalition that built it weakened; a voluntary co-op sector is exposed to the same thing. So the design question is: **what makes this hard to reverse?** Partial answers: the lock written into each co-op's own charter and backed by an independent lock share (see [The Lock](the-lock.md#how-the-lock-is-made-to-hold)), and into state co-op law, not just federal statute; a federation that becomes financially independent of the public bank; and, above all, a co-op sector large enough to have its own constituency. How each of those works, what it can't protect, and what happens now that any president can replace the bank's board, is in [Making It Last](durability.md).
|
||||
|
||||
**Money out of politics.** Legislative work pays well enough that good people want the job, but any money, gift, or benefit from a non-government entity ends the salary and starts a prison sentence. Politicians should benefit only from serving their people.
|
||||
|
||||
**But representation alone is not a guardrail.** This is the trapdoor, and it needs saying explicitly:
|
||||
|
||||
> *A government that perfectly represents its people will target a group whenever the people want that group targeted.*
|
||||
|
||||
Representation amplifies the majority; it does not restrain it. Jim Crow was representative: it represented its white voters faithfully, and that is exactly how it ground people down for a century. Internment was popular. Clean elections stop capture *by the rich*. They do nothing to stop the majority deciding a minority is the enemy.
|
||||
|
||||
**So: counter-majoritarian rights.** Things a person keeps *even when everyone around them votes to strip them away.* The whole point of a right is that it holds on the day the majority is wrong, and you cannot assume in advance that you'll always be the majority, or always be right.
|
||||
|
||||
- Keep the Bill of Rights. Make rights that protect individuals **extremely difficult to repeal.**
|
||||
- **Two tiers.** Speech, due process, equal protection, freedom from imprisonment without trial: near-absolute. Property: strong, but balanceable against the public interest with compensation, which is what the Takings Clause already says. *(This model doesn't touch existing property at all. It asks nothing of the property tier.)*
|
||||
- **The Senate had the right idea, badly built.** Keep the cooling-saucer function: a second chamber that slows things down and forces broad coalitions instead of bare-51% steamrolls. Drop the geographic weighting. Land should not get votes. Equal state representation and the protection of slavery were the *same bargain* in 1787, and that structure later shielded Jim Crow.
|
||||
- **No final guardrail exists.** Entrench rights and you've moved power to whoever *interprets* them. There's no rule so perfect it guards itself. The answer isn't a better final rule; it's **redundancy**: several overlapping checks, none supreme, so that capturing any one of them doesn't hand someone the whole apparatus.
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
title: When Work Runs Short
|
||||
section: The Co-op
|
||||
order: 23
|
||||
summary: When a co-op's business falls off, the model has everyone share the cut before anyone loses their job, with profit shares first, then fewer hours across the board, and layoffs only last, by a vote and in an order written down in advance that treats members and non-members alike.
|
||||
---
|
||||
|
||||
# When Work Runs Short
|
||||
|
||||
Every business has bad years. A conventional company usually answers with layoffs: the owners decide, and the people let go have no say. A co-op's members decide for themselves, and that cuts both ways. They can choose to share the pain. Or a majority can vote to keep its own pay and let the newest people go.
|
||||
|
||||
The founding plan says the federation's shared fund helps a co-op through a bad year ([The Federation](federation.md)). It doesn't say what the co-op itself does first. This page sets that order.
|
||||
|
||||
## What co-ops actually do
|
||||
|
||||
**They cut pay before jobs.** A study of Italian firms from 1982 to 1994, using records that match workers to their employers, found worker co-ops paid 14% less on average than conventional firms, and that their pay went up and down more while their employment went up and down less ([Pencavel, Pistaferri and Schivardi, 2006](https://econpapers.repec.org/RePEc:sae:ilrrev:v:60:y:2006:i:1:p:23-44)). A study of every co-op in Uruguay's social security records from 1996 to 2005 looked at the same question ([Burdín and Dean, 2009](https://ideas.repec.org/a/eee/jcecon/v37y2009i4p517-533.html)). Later researchers summarize it as finding the same pattern. I couldn't open the full paper to check.
|
||||
|
||||
**Mondragon moved people instead of firing them.** When Fagor, its appliance maker, went bankrupt in 2013, other Mondragon co-ops had taken on 417 of its members within two months, and around 300 more could retire early ([CECOP, January 2014](https://www.cecop.coop/works/the-mondragon-corporation-has-already-relocated-417-worker-members-from-fagor-electrodomesticos)). Two sympathetic writers reported at the time that Mondragon's own insurer, Lagun Aro, would pay members 80% of their pay for two years while it looked for places for them. They also noted that Fagor had about 2,000 workers in the Basque Country and about 3,500 more in factories in France, China, Poland and Morocco who weren't members, and that what would happen to those workers was "unclear" ([Hanna and Alperovitz, 2013](https://geo.coop/story/mondragon-and-system-problem)). Protecting members can mean pushing the cut onto everyone else.
|
||||
|
||||
**America already has a tool for sharing the cut.** Under work sharing, an employer cuts everyone's hours instead of laying some people off, and the state's unemployment insurance pays part of the lost wages. If your week goes from 40 hours to 32, you get 20% of the weekly benefit you'd have received if you'd been laid off. The Department of Labor lists 33 states with it in law, 30 of them running it ([fact sheet](https://oui.doleta.gov/unemploy/docs/factsheet/STC_FactSheet.pdf)). Under federal law it pays only for cuts in hours, not in the rate of pay, the cut has to be between 10% and 60% of the week, the state has to approve the employer's written plan first, and any health and retirement benefits the employer provides have to continue on the same terms as for everyone else ([26 U.S.C. § 3306(v)](https://www.law.cornell.edu/uscode/text/26/3306)).
|
||||
|
||||
**What the evidence can't settle.** The studies are averages over many co-ops. They don't say whether pay cuts drove away the people with the best outside offers, or whether jobs kept in bad years were worth the lower pay in good ones. The 14% gap may partly be the price of that security.
|
||||
|
||||
## The order
|
||||
|
||||
Every locked co-op's charter sets this order. A co-op can go further, but it can't skip a step.
|
||||
|
||||
1. **Profit shares fall first.** That happens on its own: when profit falls, so do the shares, and so do team bonuses that depend on results ([Does Sharing the Profit Mean Less Effort?](free-riding.md#team-bonuses-the-members-design)).
|
||||
2. **Then everyone shares the cut.** Before any layoff, the co-op cuts across the board.
|
||||
- **Pay above the going rate goes first.** Every worker has to be paid at least the going rate for their job for the hours they work ([How a Locked Co-op Is Taxed](taxes.md#wages-first)). So a cut in the *rate* of pay can only come out of pay above the going rate. Everything beyond that is a cut in hours.
|
||||
- **Salaried staff share the cut the same way, set in advance.** Staff paid a fixed salary with no overtime, usually managers and professionals, have to get their full salary for any week in which they do any work ([29 CFR 541.602](https://www.law.cornell.edu/cfr/text/29/541.602)). A slowdown can still lower it, if the lower salary is set in advance for the length of the cut and isn't adjusted week to week ([Labor Department Fact Sheet #70](https://www.dol.gov/agencies/whd/fact-sheets/70-flsa-furloughs)). So:
|
||||
- **Less pay means less work.** Their salary falls by the same percentage as everyone else's hours, with a written shorter schedule to match. Or they take whole weeks off unpaid, which the law allows since no pay is owed for a week with no work. Their hours are recorded for as long as the cut lasts. Work beyond the shorter schedule needs a manager's approval in advance and is paid on top of the salary at the old rate, which the law also allows ([29 CFR 541.604](https://www.law.cornell.edu/cfr/text/29/541.604)).
|
||||
- **Their cut keeps pace.** If hours cuts deepen, theirs follows from the next pay period. Otherwise their salary changes only when the cut starts and ends.
|
||||
- **Below the floor, they're hourly.** If the cut takes someone's salary below the federal floor of $684 a week ([29 CFR 541.600](https://www.law.cornell.edu/cfr/text/29/541.600)), or below a higher state floor, they're treated like hourly workers for as long as the cut lasts, with overtime and every other rule that goes with it. California's floor, for example, is twice the state minimum wage for full-time work ([Cal. Labor Code § 515](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=LAB§ionNum=515)), which comes to $70,304 a year in 2026, and hourly workers there also get daily overtime and meal and rest breaks.
|
||||
- **Hours cuts go through work sharing** where the state has it, so workers get part of their lost pay back from unemployment insurance, where the state covers them ([Owners Are Still Workers](labor-law.md#unemployment-insurance-covered-in-most-states-not-all)).
|
||||
- **The bottom never takes the bigger cut.** Nobody's weekly earnings can fall by a larger percentage than those of anyone paid more.
|
||||
- **Benefits stay.** Health cover and retirement contributions continue as before. That's the model's own rule, and it's also what work sharing requires.
|
||||
- **Only where the work fell off.** Where a co-op has separate lines of business that report their own results ([Investing for the Long Run](the-long-run.md#when-members-stop-investing-or-invest-badly)), the cut can be limited to the line that lost the work. Closing a whole line still needs the vote below.
|
||||
3. **Then the voluntary routes.** Before laying anyone off, the co-op offers unpaid leave with the right to come back, shorter hours for those who want them, and moves to other locked co-ops through the [federation](federation.md).
|
||||
4. **Only then layoffs,** in the order the bylaws set in advance (below).
|
||||
|
||||
**The co-op can run at a loss to keep people.** Losses are shared between the reserve and members' [capital accounts](capital-accounts.md), like profits. But pay above the going rate in a loss year is charged back to the people who got it ([Investing for the Long Run](the-long-run.md#the-rules), rule 5), so a loss can't protect high pay at the reserve's expense.
|
||||
|
||||
## Who decides
|
||||
|
||||
- **The board can act fast, once.** It can cut everyone's hours by 10% to 20% for up to 60 days without a vote, so the co-op isn't stuck waiting for a meeting while cash runs out. Salaried staff, the board's own members included, take the same percentage in the way set out above. The board can't set a different cut for itself. It can do that once in any 12 months. Anything after that needs the members. A board cut still counts in the test against [pushing people on probation out](high-turnover.md#no-pushing-people-out-before-theyd-join), whatever its size.
|
||||
- **Anything more goes to the members.** Cutting pay, cutting hours for longer or by more, and laying off anyone on the payroll, member or not, all need a vote of the members, by secret ballot, with at least 7 days' notice.
|
||||
- **A layoff always has an alternative on the ballot.** A plan that lays anyone off has to be voted on next to one that saves the same amount by cutting across everyone. The board costs both. The co-op's outside accountant checks the two costings, and the assumptions go out with the ballot.
|
||||
- **Choosing layoffs needs two majorities.** The layoff option passes only with a majority of all members and a majority of the lower-paid half, the same double vote [Who Sets the Pay](pay.md#two-majorities-not-one) uses. The lower-paid are the most likely to be let go, so they get a separate say. People on probation aren't members yet, so they don't vote, even though they're usually the first to go. Laying them off counts in the test against pushing people out before they'd join.
|
||||
- **If nothing passes, the shared cut happens.** If the board's 60 days run out with no option approved, the costed across-the-board cut takes effect, for salaried staff too. Doing nothing isn't an option when the money is running out.
|
||||
|
||||
## The layoff order
|
||||
|
||||
Every co-op's bylaws must include a written layoff order before it has anyone to lay off. The standard bylaws use seniority within each kind of job, among people who can do the work that's left. A co-op can choose other criteria by vote. The charter sets the limits.
|
||||
|
||||
- **Written ahead of time.** A change to the layoff order, or to how jobs are grouped for it, takes effect 12 months after it's adopted. So nobody can rewrite it, or regroup people's jobs, during a downturn to reach particular people.
|
||||
- **Members and non-members alike.** The same criteria apply to everyone on the payroll, and to the workers of any company the co-op controls while it's being merged in or converted ([The Lock](the-lock.md#when-a-co-op-owns-another-company)). Time counts toward seniority whether it was spent as a member, on probation or as someone who chose not to join, and so does time on medical, family or military leave. A co-op can't keep a layer of non-members to lay off first, outside the protections members get, the way Fagor's workers abroad were.
|
||||
- **Accommodation counts.** Whether someone "can do the work that's left" is judged with any reasonable change for a disability, as the law already requires.
|
||||
- **Agency and contract work ends first.** Where agency workers or contractors do the same work as people on the payroll, those contracts end before anyone on the payroll is laid off. Their hours already count in [the lock's](the-lock.md) test of who does the work, and a co-op can't use them to dodge it.
|
||||
- **The dismissal protections apply.** A layoff that doesn't follow the order can go to the same hearing and outside arbitrator as a dismissal ([Does Sharing the Profit Mean Less Effort?](free-riding.md#when-a-member-wont-do-the-work)). The rules on retaliation, disability and leave apply too. If someone laid off had organized, filed a complaint or led opposition in a vote within the last 12 months, the co-op has to show it followed the order.
|
||||
- **Notice.** At least 30 days' notice, or pay instead, or 14 days in a co-op of fewer than 15 workers. The law often requires more. Under federal law, employers with 100 or more full-time workers give 60 days' notice of large layoffs and closings, and cutting someone's hours by more than half for six months counts as a layoff ([29 U.S.C. § 2101](https://www.law.cornell.edu/uscode/text/29/2101), [§ 2102](https://www.law.cornell.edu/uscode/text/29/2102)). A long, deep work-sharing cut can trigger it. Some states require 90 days, including New York for employers with 50 or more full-time workers there ([Cooley, 2023](https://www.cooley.com/news/insight/2023/2023-08-03-new-york-state-amends-its-warn-act-regulations)) and New Jersey, which also requires severance ([Jones Day, 2023](https://www.jonesday.com/en/insights/2023/04/new-jersey-warn-act-amendments)).
|
||||
- **Severance** follows the bylaws, and never less than state law requires. When a co-op closes, notice pay and severance from different laws don't add up; the worker gets the largest of each ([Protecting Members' Savings](savings-guarantee.md#notice-pay-and-severance)). The standard bylaws pay one week per year worked, the same as New Jersey requires of larger employers in mass layoffs and closings. More than two weeks per year, or more than 26 weeks in all, counts as pay under [the pay rules](the-long-run.md#the-rules).
|
||||
- **Lopsided layoffs get checked.** Federal agencies treat a selection rate for any race, sex or ethnic group below four-fifths of the rate for the group doing best as a sign of possible discrimination ([29 C.F.R. § 1607.4(D)](https://www.law.cornell.edu/cfr/text/29/1607.4)). The model uses the same line for layoffs, and adds age. If the share of any gender, race or age group keeping their jobs falls below it, the co-op publishes a written explanation before the layoffs take effect, and anyone laid off can take it to the outside arbitrator. The check looks at the criteria, not at who they picked. A co-op can drop a criterion that isn't needed for the work, for everyone, but it can't change who's laid off because of their race, sex or age, which employment law forbids too ([*Ricci v. DeStefano*, 2009](https://www.law.cornell.edu/supremecourt/text/07-1428)).
|
||||
|
||||
## Coming back
|
||||
|
||||
- **A right to be recalled for 12 months,** in reverse order of the layoff, to the same job at the same hours or more. Until everyone laid off from a job has been offered it back, the co-op can't hire anyone new into that job, under any title, or hand the work to a contractor. Someone who doesn't accept within 14 days drops to the back of the list.
|
||||
- **Membership comes back with them.** A member who returns within 12 months doesn't start probation again, and goes back to the same step on the [profit share ramp](worker-ownership.md).
|
||||
- **So do their savings.** A laid-off member's capital account is fixed and paid on the normal schedule, like anyone who leaves ([Member Capital Accounts](capital-accounts.md)). If they come back, whatever hasn't been paid out yet becomes their account again and counts toward the buy-in.
|
||||
|
||||
## Unemployment insurance
|
||||
|
||||
People laid off from a co-op claim unemployment insurance like anyone else, where their state covers co-op members ([Owners Are Still Workers](labor-law.md#unemployment-insurance-covered-in-most-states-not-all)). Members can't lay themselves off in turns to collect it, except through the state's work-sharing program. That's the abuse some states cite for leaving co-op owners out, and the model gives it up to keep members covered. The outside accountant checks claims against the schedule. And like any employer, a co-op whose workers claim benefits pays a higher unemployment tax rate afterward.
|
||||
|
||||
## When cutting isn't enough
|
||||
|
||||
Sharing the cut buys time. It doesn't save a business nobody wants to buy from. Help from the federation's fund comes with a recovery plan and a limit agreed before any crisis ([Investing for the Long Run](the-long-run.md#when-members-stop-investing-or-invest-badly)). Help for the workers, like finding them places in other co-ops, has no such limit. **No working on credit.** Once a co-op misses a payroll by more than five business days, it can't schedule more ordinary work until everyone is paid up, unless it files for bankruptcy protection or the federation's fund lends it the payroll. Safety, care and wind-down work goes on, paid by the pool. Federal law, and some states, can make officers and even members pay wages the co-op doesn't ([Owners Are Still Workers](labor-law.md#when-wages-go-unpaid-members-can-owe-them)), so this protects them as well as the people owed.
|
||||
|
||||
If the co-op fails, [the insurance pool](savings-guarantee.md) covers workers' last paychecks and part of their savings, and [When a Co-op Goes Bankrupt](bankruptcy.md) covers what happens to the business.
|
||||
|
||||
## Measuring it
|
||||
|
||||
The pilot publishes, for each co-op's bad years compared with matched conventional firms: changes in hours, pay and headcount; layoffs by member status, gender, race and age; how many people were recalled; and how many laid-off members found work in other co-ops. If co-ops turn out to protect members by letting non-members go, that shows up in the figures.
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **Cuts drive some people away.** The people with the best outside offers leave first. Cuts to pay above the going rate fall on the better-paid, and in a loss year [rule 5](the-long-run.md#the-rules) charges them again, out of their accounts. A co-op can keep every job and still lose the skills it needs most.
|
||||
- **Sharing the cut can keep a dying business going.** Everyone taking less for years can put off a closing that should have happened, while the reserve and members' accounts run down.
|
||||
- **Work sharing isn't everywhere.** In the states without it, sharing the cut means lost pay with no insurance to make up part of it, and layoffs look better by comparison.
|
||||
- **The shock still lands on someone.** Ending agency and contract work first protects the co-op's own workers by pushing the cut onto people outside it. That's what every employer does, and it's close to what happened to Fagor's workers abroad.
|
||||
- **Hiring gets more cautious.** Recall rights, notice and the vote all make adding people in good years a bigger commitment. Co-ops will likely hire more slowly, which the evidence already suggests they do.
|
||||
- **Members can still choose layoffs.** Two majorities make it harder, not impossible. The newest members are the easiest to vote out of a job.
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
title: Making It Last
|
||||
section: Guardrails
|
||||
order: 125
|
||||
summary: Anything built by one law can be undone by the next, so the model is built to survive a hostile government, and this page says what that protects and what it can't.
|
||||
---
|
||||
|
||||
# Making It Last
|
||||
|
||||
The biggest threat to this model isn't that the state grows too strong. It's that a later government undoes it. Everything here rests on ordinary laws: a charter for [the bank](the-bank.md), a law backing [the lock](the-lock.md), and money set aside for [the pilot](pilot-spec.md). Anything one Congress passes, a later one can repeal ([The Constitution](constitution.md)).
|
||||
|
||||
Repeal is only the most obvious way to kill it. Public programs die in quieter ways too. They get starved of money and staff, run by people who want them to fail, or left without anyone able to sign off on decisions. A hostile government can also make an experiment *look* like it failed. This page goes through how those things have happened to real programs, and what the model does about each one. Some of it can be protected well. Some of it can't be protected at all, and the page says which is which.
|
||||
|
||||
## How public programs get killed
|
||||
|
||||
**By doing nothing: the Export-Import Bank.** America's export-credit bank has to be reauthorized by Congress every few years. In 2015 Congress let its charter lapse, and from July 1 until early December it couldn't make new loans. Then a second problem hit. Its board needs three voting members to act, and when two terms expired in July 2015, the Senate didn't confirm replacements. For nearly four years the bank couldn't approve any deal over $10 million. By mid-2016, more than 30 deals worth over $20 billion were stuck waiting ([Congressional Research Service, 2016](https://www.everycrsreport.com/reports/IN10574.html); [2019](https://www.everycrsreport.com/files/20190809_R43581_3f42ff567900d03421b053fdee99f7450d886877.html)). Nobody had to vote to shut it down. Opponents only had to not act.
|
||||
|
||||
**By cutting its staff: the CDFI Fund.** The Treasury's Community Development Financial Institutions Fund has backed community lenders for three decades. In March 2025, a presidential order told it to shrink to "the minimum presence and function required by law." In October 2025 the Treasury moved to lay off its roughly 100 staff. 105 Republican members of Congress wrote to defend it, and a court order blocking layoffs during that fall's government shutdown held them off for the time being ([Roll Call, 2025](https://rollcall.com/2025/10/27/senate-and-house-gop-defend-community-finance-fund-from-trump-cuts/)). A program that runs on the government's staff can be starved without being repealed.
|
||||
|
||||
**By taking the money back: the Greenhouse Gas Reduction Fund.** Congress created this $27 billion fund in 2022 to seed green lenders. By early 2025, $20 billion had been awarded to eight nonprofit lenders and was held at Citibank, which acted as the government's financial agent. In March 2025 the new administration terminated the grants and froze the money. The courts have gone back and forth, and the case is still going ([Columbia Law School Climate Law Blog, 2026](https://blogs.law.columbia.edu/climatechange/2026/08/07/a-big-win-in-few-words-what-to-glean-from-the-climate-united-fund-decision/)). Then, in July 2025, Congress repealed the program and took back all of its money that hadn't yet been committed. It did that inside a huge budget bill, passed by simple majorities under special rules that block a Senate filibuster. Because almost all of the money had already been awarded, the repeal itself took back only about $19 million ([Congressional Record, July 9, 2025](https://www.congress.gov/119/crec/2025/07/09/171/118/modified/CREC-2025-07-09-pt1-PgS4281-4.htm)). Two lessons: money the executive branch can reach can be frozen with a letter, and money not yet committed can be taken back by a line in a bill few people read. Money already awarded was much harder to take back.
|
||||
|
||||
**By changing what it is: America's co-op bank.** When the Reagan administration tried to close the National Consumer Cooperative Bank in 1981, it survived by being privatized. Then it drifted toward lending to co-ops that could have borrowed anywhere ([Has This Been Tried?](precedents.md#americas-co-op-bank-the-national-consumer-cooperative-bank)).
|
||||
|
||||
## How public programs survive
|
||||
|
||||
**A fair trial and a steady base: the Bank of North Dakota.** North Dakota's state-owned bank opened in 1919, created by a farmers' movement openly sympathetic to socialism. Its opponents fought it every way they could. They sued and lost at the US Supreme Court in 1920. They recalled the governor in 1921, the first governor recall in American history. They put abolishing the bank on the ballot, and lost by 4,238 votes. They worked with East Coast financiers to boycott the state's bonds, and that boycott, along with the recall, helped push the movement that founded the bank out of power. What saved the bank itself, historians say, was the governor from the opposing side who replaced the recalled one. He decided it was "right and just and wise to give it a full, fair, and honest trial," and ran it "in a sane and conservative way" ([Bank of North Dakota history](https://thebndstory.nd.gov/years-of-desperation/organized-opposition-tried-to-take-the-bank-down/)). It also has a structural advantage: by state law, the state's own money must be deposited with it ([N.D. Century Code § 6-09-07](https://www.ndlegis.gov/cencode/t06c09.pdf)). It's still running more than a century later. It survived by being careful, which this model's bank is designed to be from the start.
|
||||
|
||||
**Owners spread everywhere: rural electric co-ops.** In the mid-1930s, nine in ten rural American homes had no electricity. Private utilities wouldn't wire sparse areas. From 1936, the federal Rural Electrification Administration lent money to farmers who formed co-ops to do it themselves. By 1953, over 90% of farms had power. In 1942, facing false accusations during the war, the co-ops formed a national association to speak for them in Washington. Today more than 900 electric co-ops serve over 40 million people ([America's Electric Cooperatives](https://www.electric.coop/our-organization/history); [2026](https://www.electric.coop/rea-at-90-years-celebrating-the-landmark-law-that-birthed-rural-electrification)). The program built its own defenders: owners in almost every congressional district. That cuts both ways. Critics can fairly say that defenders like these keep subsidized loans flowing long after the original job is done.
|
||||
|
||||
**Paying the government back: the Farm Credit System.** Congress started a system of farmer-owned co-op lenders in 1916 and expanded it in 1933, with federal money. The lenders repaid all of it, and by 1968 they were wholly owned by the farmers who borrowed from them ([Farm Credit Administration](https://www.fca.gov/about/historical-highlights-of-fca-and-the-fcs)). Once the system belongs to its borrowers, no budget vote can starve it and no president can replace its lenders' boards. It wasn't the end of the risk: in the 1980s farm crisis the system needed a federal rescue, of which about $1.3 billion was used.
|
||||
|
||||
**What political scientists find.** Eric Patashnik studied what happens to major reforms after they pass. He found that "the reforms that stick destroy an existing policy subsystem and reconfigure the political dynamic," creating their own supporters ([Reforms at Risk, Princeton University Press](https://press.princeton.edu/books/paperback/9780691138978/reforms-at-risk)). Reforms that leave the old interests in place tend to unravel.
|
||||
|
||||
## Who controls the bank now
|
||||
|
||||
The model says the bank should be walled off from politicians "the way monetary policy is walled off from Congress." The obvious way to do that is to give the bank's board fixed terms, so a president can remove them only for misconduct. That route is now closed.
|
||||
|
||||
In June 2026, in *Trump v. Slaughter*, the Supreme Court held that the Federal Trade Commission's protection from removal was unconstitutional, because officials under the President's "general administrative control" must be removable at will. It put aside only the Federal Reserve, "to the extent that it follows in the distinct historical tradition of the First and Second Banks of the United States—both of which influenced monetary policy" ([Trump v. Slaughter, 2026](https://www.supremecourt.gov/opinions/25pdf/25-332_qn12.pdf)). The same day, in *Trump v. Cook*, the Court upheld the Fed governors' protection, on the ground that "monetary policy should not be subject to political interference" ([Trump v. Cook, 2026](https://www.supremecourt.gov/opinions/25pdf/25a312_5468.pdf)). A bank that lends to co-ops doesn't set monetary policy, so it almost certainly doesn't fit that exception.
|
||||
|
||||
So this model assumes **any president can replace the bank's whole board, any time.** The protection has to come from what the board *can't* do, not from who sits on it. That's also more honest. A bank run by people nobody can remove would be exactly the unaccountable power this model warns against elsewhere.
|
||||
|
||||
## The rules
|
||||
|
||||
### What a new board can't change
|
||||
|
||||
The things that decide whether the experiment is fair are written into the law that creates the bank, not left to the board:
|
||||
|
||||
- the [co-op definition and the lock](worker-ownership.md) that borrowers must accept;
|
||||
- the measures, thresholds, reviewers and deadlines in [the decision rule](pilot-spec.md#the-decision-rule);
|
||||
- **the lending standards**, meaning the scoring model used to judge applications, filed with the rest of the plan before the first loan;
|
||||
- the [rate audit](pilot-spec.md#what-market-rate-means), which works in both directions;
|
||||
- the loan limits and the rules against [political favoritism](pilot-safeguards.md#political-favoritism);
|
||||
- **how loans are managed once made.** A loan can be called in early only if payments stop, never for a technicality. The rules for handling a co-op in trouble are filed in advance too.
|
||||
|
||||
The board can propose changes to the lending standards, with published reasons. The reviewers then report every result both ways: under the original standards and under the new ones. By law, nobody outside the bank can direct a decision on a particular loan, except the President through the board he appoints. Every instruction or contact the bank receives about a particular loan is logged and published. The board itself can overrule its staff on a loan, because the Constitution probably requires that, but only in writing and in public ([Who has the last word](#who-has-the-last-word)).
|
||||
|
||||
### Rigging the result
|
||||
|
||||
A hostile board has two ways to make co-ops look like failures without breaking any rule openly. The reviewers check for both.
|
||||
|
||||
- **Turning away good applicants.** Every year, the reviewers re-score the bank's rejected applications against the filed standards and publish the rejection rate for applications that met them. Where the line falls is set in advance ([below](#when-rejections-mean-not-tested)). If too few co-ops get funded because applicants didn't show up, that's a real result, as [the pilot](pilot-spec.md#why-about-400-co-ops) says. If too few get funded because the bank turned away applicants who met its own standards, the final verdict says so: **"not tested,"** not "failed." The audit also covers co-ops in lower-paid work separately, so the bank can't quietly drift toward the safest co-ops ([Who the Bank Lends To](who-gets-loans.md)).
|
||||
- **Making bad loans on purpose.** If the [emergency stop](pilot-spec.md#the-decision-rule) fires, the reviewers check whether the losses come mainly from loans that broke the filed standards. If they do, it's treated like fraud, not like a verdict on co-ops. Lending restarts under the original standards, and those loans are reported separately.
|
||||
|
||||
**This isn't a way to keep the bank alive.** "Not tested" means exactly what "failed" means for the bank: no new loans, the wind-down, and no second round of money. It only changes what the record says about co-ops. Nothing on this page extends the timeline either. The latest the final review can happen is year 15, which is the pilot's existing rule for when good applications are scarce.
|
||||
|
||||
### When rejections mean "not tested"
|
||||
|
||||
"Turned away applicants who met its own standards" needs a line drawn in advance, or a board could argue about it forever. The line is filed with the rest of the plan before the first loan.
|
||||
|
||||
**What the reviewers check.** Every year they re-score every complete application the bank rejected, and every one it approved. They also re-score a random sample of applications that were withdrawn, left incomplete or passed the 120-day limit, since pushing people to give up is another way of saying no. Checking every file, not a sample, removes the argument about sampling luck. It costs something, perhaps a few hundred thousand dollars a year if the bank decides several hundred applications, and that cost counts against the bank's money test like any other.
|
||||
|
||||
They score each file with the filed scoring model, from what the applicant submitted plus the checks the standards call for, like credit reports. Any change a loan officer made to the applicant's numbers counts only if it's a reason on the filed list. Paperwork made after the decision, like closing documents, is removed first, so the reviewers can't tell which files were approved.
|
||||
|
||||
Around the bank's approval line there's a band where the bank decides by lottery ([the lottery at the margin](pilot-spec.md#who-co-ops-are-compared-against)), and honest judgment calls fall there. A rejection counts as **wrongful** if both of these are true:
|
||||
|
||||
- the reviewers' score is above the top of that band;
|
||||
- the file doesn't show a reason from the filed list of disqualifiers that the reviewers find is actually backed by the file. Examples are a false statement, a sham co-op, or a missing document. A missing document counts only if it's on a standard checklist every applicant gets, with a deadline filed in advance. A reason that isn't on the list doesn't count, however sensible it sounds. The bank can add reasons to the list, but only publicly and only for later decisions, the same way it changes any [lending standard](#what-a-new-board-cant-change).
|
||||
|
||||
Every rejection notice names the specific reason, which federal fair-lending law already requires for most small-business applicants and makes available on request for larger ones ([12 CFR 1002.9](https://www.law.cornell.edu/cfr/text/12/1002.9)), and also gives the applicant their score.
|
||||
|
||||
**The line.** The bank fails a year's audit on either of two tests:
|
||||
|
||||
1. **Too many turned away.** More than 1 in 10 of the applications scoring above the band were rejected wrongfully.
|
||||
2. **The best kept out.** Wrongly rejected applicants scored higher than the approved applicants who also scored above the band, by more than chance explains. That comparison leaves out the lottery winners from inside the band, who would always score lower and make an honest bank look guilty.
|
||||
|
||||
Co-ops in lower-paid work are held to the same line separately ([Who the Bank Lends To](who-gets-loans.md#money-set-aside)), and the yearly report also breaks the rejection figures down by the founders' race, ethnicity and sex. The report also publishes how often each disqualifier was used, so a sudden jump in "sham co-op" or "missing document" stands out.
|
||||
|
||||
**The same line for approvals.** Rejections aren't the only way to rig a test. Approvals scored below the band, outside the lottery, with no filed reason, are counted too, and more than 1 in 10 fails the audit the same way. Those loans are reported separately, like loans that [broke the standards](#rigging-the-result). This keeps the audit from pushing loan officers toward saying yes to everything.
|
||||
|
||||
**Why 1 in 10.** There's no good benchmark for how often a lender wrongly turns away a clear pass, so 1 in 10 is a judgment call, set loose on purpose. Small-business loans are far less standardized than mortgages, and the lottery band already absorbs the close calls. Even so, when a bank is breaking its own rules on more than 1 clear case in 10, it isn't running the test the law describes. For approvals there's loose context: when mortgage lenders re-check loans they've already approved, they find serious errors in about 1.5% of them ([ACES, 2025](https://www.send2press.com/wire/aces-q4-and-cy-2025-mortgage-qc-industry-trends-report-shows-critical-defect-rate-falls-to-annual-low/)). That's a much more routine kind of loan, which is part of why the line here is set far looser.
|
||||
|
||||
**Fixing it, not just labeling it.** Every applicant found wrongly rejected, in any year, is offered a new decision within 60 days. It's made on the reviewers' score, and at staff level only a disqualifier the reviewers confirm can block it. The board can still overrule it in writing, but that counts against the bank in the audit ([Who has the last word](#who-has-the-last-word)). That undoes most of the damage while the pilot is running. The fix isn't a lending target. It only brings in applicants who met standards the bank filed itself. Loans made this way count in full in both deciding measures. They're also reported separately, since a group that waited a year may not be the group it was.
|
||||
|
||||
**What "not tested" then takes.** At the final review, the verdict is "not tested" instead of "failed" only if all three of these hold:
|
||||
|
||||
1. the pilot failed its test;
|
||||
2. the bank failed the audit in at least one year;
|
||||
3. the wrongful rejections that were never fixed could have changed the result. Those are the ones where the group turned down the new offer, had broken up, never got one, or had the new decision overruled by the board. The reviewers add them back as if each had done as well as funded co-ops with similar scores. If that would turn the survival result into a pass, the verdict is "not tested." If not, it stays "failed."
|
||||
|
||||
Otherwise a fail stays a fail. A pass stands too, since rigging that didn't work doesn't change what the co-ops did. And "not tested" gives the bank no claim to the fast-track vote or to new money.
|
||||
|
||||
**It can't easily be used to dodge a fail.** A friendly board can't see a fail coming and use this rule to hide it. The verdict comes years after lending ends, every wrongful rejection is offered a new decision, and step 3 asks whether the missing co-ops would have changed the numbers, not just whether some were missing. Rejecting good applicants on purpose to set up that excuse would also have to happen in writing, because an unrecorded reason counts against the bank. Any instruction to reject a particular application is covered by the rules against [political favoritism](pilot-safeguards.md#political-favoritism).
|
||||
|
||||
**What it can't catch.**
|
||||
|
||||
- **Scoring can be wrong.** If the filed scoring model itself counts against some founders more than it predicts repayment, re-scoring with the same model won't show it. That's checked separately ([Who the Bank Lends To](who-gets-loans.md#treated-the-same)).
|
||||
- **People who never apply.** An audit of files can't see people who never applied because the bank was slow, hard to reach or cut its outreach. The published application and approval rates ([After a Pass](scale.md)) are the only check there.
|
||||
- **What isn't written down.** Scoring from the file can't see what a loan officer learned in a meeting and didn't write down. That cuts both ways: a bank that wants its reasons counted has to write them down.
|
||||
|
||||
### No quorum, no freeze
|
||||
|
||||
Unlike the Export-Import Bank, this bank can't be frozen by leaving board seats empty. It uses ordinary delegation, which keeps the President in charge:
|
||||
|
||||
- **Standing authority for smaller loans.** The board appoints senior credit officers and gives them authority to approve loans up to $1 million under the filed standards. That authority doesn't lapse when board seats go empty. Any board can take it back, publicly, but it can't disappear by default.
|
||||
- **No quorum means an acting chair, not no one.** If the board can't meet, the Treasury Secretary acts for it, but only on loans too big for staff and on reviewing staff decisions. The bank's own law has to say so. The general federal law on acting officials doesn't cover seats on the board of a government corporation ([5 U.S.C. § 3349c](https://www.law.cornell.edu/uscode/text/5/3349c)). Every rule on the board's own overrules ([below](#who-has-the-last-word)) applies to the Secretary too, and every decision made without a quorum is flagged in the results. The Secretary can't change the lending standards, and can take away a credit officer's authority only for cause, in writing.
|
||||
- **Emptying the board concentrates power. It doesn't hide it.** A president who fires the board and names no one puts its review power in one cabinet officer's hands. That's better than a freeze, and every use of it is public, but it's a single point of control. The results report how long each vacancy lasted and whether the quorum was lost through firings or through terms running out.
|
||||
- **Applications can't be buried.** A complete application gets a decision within 120 days, yes or no, with reasons. The clock pauses once, while the applicant answers the bank's one round of follow-up questions ([below](#who-can-go-to-court)). The rule forces an answer, not a yes.
|
||||
- **A second check during vacancies.** Any loan over $500,000 approved while the board lacks a quorum is checked by the independent auditor before any money goes out. If the auditor objects, the loan waits for the Treasury Secretary's written decision, which is published with the auditor's objection. If the Secretary hasn't decided within 30 days, the loan doesn't go out. It counts as a rejection by the bank, including in the rejection audit if the applicant scored above the lottery band.
|
||||
|
||||
### Who has the last word
|
||||
|
||||
The Constitution limits how far a lender like this can be insulated from the people the President appoints. Anyone who makes final decisions for the government has to answer, at some level, to officials the President appointed and the Senate confirmed. In 2021 the Supreme Court struck down a rule that stopped the head of the Patent Office from reviewing decisions by the judges below him, because "the unreviewable authority" of those judges was "incompatible with their appointment" as lower-level officers ([United States v. Arthrex, 2021](https://www.law.cornell.edu/supremecourt/text/19-1434)). The bank's credit officers hold permanent jobs with the power to commit up to $1 million of federal money on their own signature. Courts might well treat them the same way. That case was about officials deciding disputes, not lenders, and the credit officers might instead count as ordinary employees. The design below works either way.
|
||||
|
||||
So the design gives the last word to the board, and makes using it visible:
|
||||
|
||||
- **The board appoints the credit officers.** The board of a freestanding federal body likely counts as the "head of a department" that can appoint lower-level officers ([Free Enterprise Fund v. PCAOB, 2010](https://www.law.cornell.edu/supremecourt/text/08-861)). That case was about the SEC. Whether the board of a government corporation counts the same way hasn't been decided.
|
||||
- **The board can review any staff decision,** approvals and rejections alike. A staff decision isn't signed for 15 days unless the board waives review of it in writing. Those days count inside the 120-day deadline, and inside the 60 days for new decisions after the audit. That way the board never has to cancel a signed loan.
|
||||
- **Every overrule has names on it.** It's a recorded vote showing how each member voted, with reasons given file by file, never one memo for a batch. It's published along with any outside contact about that file. Loans the board decided over its staff's heads are reported as their own group. A board rejection of an applicant who scored above the lottery band counts in the [rejection audit](#when-rejections-mean-not-tested) like any other. A board approval of an applicant who scored below it, outside the lottery, counts in the approval check and falls under the rules against [political favoritism](pilot-safeguards.md#political-favoritism).
|
||||
- **What that protects.** A board can block good loans or push weak ones, one at a time, but each is public. Enough blocked loans can make a fail "not tested," and pushed loans that go bad show up as broken standards if the [emergency stop](pilot-spec.md#the-decision-rule) fires. That protects the record, not the co-ops turned away. "Not tested" ends the bank just as a fail does.
|
||||
- **Credit officers can be fired only for cause,** such as misconduct or breaking the filed standards. Only the board can fire them, with written findings that are published. For 140 years the Supreme Court has let Congress limit the firing of officials appointed by a department head ([United States v. Perkins, 1886](https://www.law.cornell.edu/supremecourt/text/116/483)). In July 2026 a federal trial court in Virginia held that this survives *Slaughter* (*Comans v. Executive Office of the President*, [reported by Lawfare](https://www.lawfaremedia.org/article/inferior-officers-protections-post-slaughter)). That's one court, and it binds no other.
|
||||
- The administration argues the rule covers only officers with "limited duties and no policymaking or administrative authority," a line from [Seila Law v. CFPB, 2020](https://www.law.cornell.edu/supremecourt/text/19-7). Officers who can commit $1 million on their own signature may not fit that, so losing is at least as likely as winning.
|
||||
- If it's lost, credit officers can be fired at will. Either way, the board can take back any officer's lending authority at any time, in public.
|
||||
- **The President supervises through the board.** The law can't stop a president directing the board he can fire, or replacing it with people who need no instructions. What it can do is put a duty on the bank's side. Board members and staff log and publish every instruction or contact they get from anyone in the executive branch, the White House included, about a particular loan, applicant or group of applicants. Contacts from members of Congress are handled the same way.
|
||||
- What this can't catch: orders given by phone and never mentioned, or a board replaced with people who agree. A law requiring the President's own messages to be published could also run into executive privilege. This rule catches careless pressure, not careful pressure.
|
||||
|
||||
**The same goes for the outside referees.** The reviewers and the auditor are private contractors. Private bodies can advise the government, but they can't have the final say over what it does. In 2025 the Supreme Court upheld a private body's role in the phone-subsidy program because its recommendations "cannot go into effect without an agency's say-so" ([FCC v. Consumers' Research, 2025](https://www.law.cornell.edu/supremecourt/text/24-354)). So:
|
||||
|
||||
- **The reviewers' score binds the staff, not the board.** Suppose the rejection audit finds an applicant was wrongly turned away. The new decision is made by a credit officer on the reviewers' score, following a rule the law sets for them. It then goes through the same 15-day board review as any other decision.
|
||||
- The board can overrule it, in writing. The overrule counts as a wrongful rejection in the audit unless it names a disqualifier on the filed list, and the reviewers confirm the file backs it.
|
||||
- That count is a report, not a veto. It measures whether the filed standards were applied, whoever applied them.
|
||||
- A court might hold that even this gives the reviewers too much say. Then the score becomes advice to the staff, and a staff refusal simply counts in the audit the same way.
|
||||
- **The results are reports. The board adopts them.** The reviewers' "failed" or "not tested" is what the record says, and on its own it orders no one. New lending ends on the date the law sets. Within 60 days of the final report, the board formally adopts the result. It can reject the reviewers' calculation only by publishing the specific error in how the filed rule was applied, which the auditor then checks in public. Once the result is adopted, a fail starts the loan sale, and a pass gives Congress its fast-track vote. Carrying out the sale after a fail is a duty with a deadline, so a court can order it. A friendly board could still refuse a fail on a made-up error, but only in writing, with the auditor's answer beside it.
|
||||
- **Restarting after an emergency stop is up to the board.** The board decides in writing within 30 days of the reviewers' report. If the report says restart and the board refuses, it has to say so in public. A hostile board can end lending this way, and that can't be closed without taking the last word away from the board.
|
||||
|
||||
### Where the money sits
|
||||
|
||||
The $350 million, and the separate $100 million for [worker buyouts](buyouts.md), is paid into the bank as its own capital when it's chartered, not handed over year by year. The model says plainly what that does and doesn't protect.
|
||||
|
||||
- **It needs explicit legal cover.** By default, the Treasury keeps a federal corporation's accounts ([31 U.S.C. § 9107](https://www.law.cornell.edu/uscode/text/31/9107)). The farm co-op banks were exempted from that rule by name, and this bank's law has to do the same, so its money isn't held where one agency can freeze it.
|
||||
- **Running costs come from the bank's capital and income**, as [the pilot](pilot-spec.md#how-much-money-and-why) already says, not from yearly appropriations. So nobody can starve it by refusing to pass a budget line.
|
||||
- **The reviewers are paid in advance.** Money for the independent review is set aside from the capital on day one and paid to the reviewers by contract. A hostile government can't quietly disable the referee by not funding it.
|
||||
- **What it doesn't protect.** Money not yet lent can still be taken back by Congress, the way the green-bank program's uncommitted money was. That takes a law, and it happens in public, but it can be a single line in a large budget bill passed by a simple majority. The pilot's capital will sit partly unlent for years, and that's exactly the kind of money that gets taken. Congress can also attach riders to spending bills that bar the bank from using its money. Neither can be prevented. The only defense is that doing it is visible.
|
||||
|
||||
### Loans already made keep their terms
|
||||
|
||||
Every loan is a binding contract. If a later law ends the bank, money already lent keeps its terms, and the loans are offered first to co-op and community lenders, as they would be after a failed test (see [the decision rule](pilot-spec.md#the-decision-rule)).
|
||||
|
||||
The wind-down has its own guards. Loans are sold for no less than an independent appraisal, only to lenders named in the bank's law (co-op lenders, credit unions and nonprofit community lenders), and they can't be resold to anyone else for five years. Otherwise a hostile Treasury could sell them cheaply to a front for a distressed-debt buyer.
|
||||
|
||||
Future stages of a loan that haven't been paid out are *not* promised. The contract says so in plain words. If Congress ends the bank, it doesn't owe anyone money for loans it never made, and one Congress can't lock a later one into paying out.
|
||||
|
||||
If a later law tried to change the terms of money already lent, there's precedent for the government owing damages. In *United States v. Winstar* (1996), Congress changed banking rules in a way that broke promises regulators had made to savings-and-loan companies that took over failing ones. The Court's lead opinion held that Congress was free to change the law, but the government still owed damages for breaking its contracts ([Winstar, 1996](https://www.law.cornell.edu/supremecourt/text/518/839)). It's only partial protection. There was no single majority opinion, the damages cases dragged on for years, and it only helps where the contract clearly puts the risk of a change in the law on the government. So the loan contracts say that explicitly.
|
||||
|
||||
### Who can go to court
|
||||
|
||||
Most of the rules above work without a judge. The verdict's wording follows the reviewers' findings, and the reviewers are paid in advance. The reports don't need a court either: if the bank doesn't publish its figures on time, the reviewers, who get the same data, publish them. But a few rules only work if someone can enforce them: deciding applications within 120 days, making the new decisions the [rejection audit](#when-rejections-mean-not-tested) requires, and paying out loans already signed. If a hostile board simply ignores them, someone has to be able to take it to court, and win something more useful than money years later.
|
||||
|
||||
Recent cases show the risk. In 2025 the Supreme Court said claims to restore cancelled research grants likely belonged in the Court of Federal Claims, which can award money but can't order a program to keep running ([NIH v. American Public Health Association, 2025](https://www.law.cornell.edu/supremecourt/text/25A103)). In the green-bank case, a panel of the D.C. Circuit said the same in 2025. In August 2026 the full court voted 6 to 4 to keep the order blocking the grants' cancellation, and the government is taking the case to the Supreme Court ([D.C. Circuit judgment, 2026](https://media.cadc.uscourts.gov/judgments/docs/2026/08/25-5122-2186441.pdf)). Which court can hear these cases, and what it can order, is itself being fought over. So the bank's law settles it for this bank instead of leaving it to the general rules.
|
||||
|
||||
**What a court can order.**
|
||||
|
||||
- **Only duties with a deadline.** The law says the bank *must* decide a complete application within 120 days, and must make each new decision the audit calls for within 60 days. Courts can order an agency to take a specific action the law requires of it ([Norton v. Southern Utah Wilderness Alliance, 2004](https://www.law.cornell.edu/supremecourt/text/03-101)). A court checks whether the duty was met on time, never whether a rejection was right. The reasons are checked by the [rejection audit](#when-rejections-mean-not-tested), not by judges. The one exception is a new decision after the audit. The law already says the staff's answer there is yes unless the reviewers confirm a disqualifier, so a court can probably order the staff to follow that rule. The board keeps its 15-day review of the result, and a written overrule by the board stands. It's counted in the audit instead.
|
||||
- **No endless paperwork.** Whether an application is complete is judged against the bank's published checklist. The bank gets one round of follow-up questions, and the application is complete once the applicant answers them. Otherwise the 120-day clock could be stopped forever and there'd be nothing for a court to order.
|
||||
- **Signed loans, from money that's still there.** A borrower can make the bank pay out a stage of a signed loan once that stage's written conditions are met. Those conditions have to be objective and written into the contract, not left to the bank's judgment. The court can order payment only from the bank's own capital. If a later law, including a rider in a spending bill, bars the payment, no court can order it ([OPM v. Richmond, 1990](https://www.law.cornell.edu/supremecourt/text/496/414)). That's the same line drawn [above](#loans-already-made-keep-their-terms).
|
||||
- **The right court.** The law lets the government be sued for these duties in federal district court, which can order the bank to act, not only award money, even where the general rules would send the case to the Court of Federal Claims. Congress decides where the government can be sued and for what. It hasn't been tested, and a later Congress could repeal it, but only by passing a law.
|
||||
|
||||
**No friendly settlements.** No court order or settlement can require the bank to approve a loan, apart from following the audit's own rule, or change a filed standard. Every settlement is published and reported to Congress. That stops a friendly board from inviting a lawsuit and agreeing to loosen its own standards, and it stops a hostile one from settling its duties away.
|
||||
|
||||
**Who can sue.** Applicants and borrowers who were harmed. Since 2025, a court's order usually helps only the people who sued ([Trump v. CASA, 2025](https://www.supremecourt.gov/opinions/24pdf/24a884_8n59.pdf)). Class actions may still be possible against a bank-wide practice, like a freeze on all decisions, but probably not over many different files. The federation can sue for a member co-op that was harmed, as membership groups already can, but that doesn't help groups that aren't members. Courts must hear deadline cases quickly, since a founding group waiting on a decision can break up long before a normal lawsuit ends.
|
||||
|
||||
**Legal costs.** A founding group can't afford to sue a federal bank. Small businesses and co-ops that win against the government can sometimes recover their lawyers' fees, but at capped rates, and not if the government's position was "substantially justified" ([28 U.S.C. § 2412](https://www.law.cornell.edu/uscode/text/28/2412)). So when the bank has missed a deadline, which is a plain fact, it pays the winner's reasonable fees in full. All of the bank's legal costs count against its money test, win or lose, like any other running cost.
|
||||
|
||||
**If the government won't defend the law.** Someone could challenge the bank's law itself, and a hostile Justice Department could decline to defend it, or settle. The Attorney General already has to tell Congress within 30 days of deciding not to defend a law ([28 U.S.C. § 530D](https://www.law.cornell.edu/uscode/text/28/530D)). The bank's law adds settlements to that, and requires notice to the federation, the lock-holders and every borrower. After that, the options are weak:
|
||||
|
||||
- The House defended the Defense of Marriage Act in 2013, but the Supreme Court never decided whether it had the right to. The Court has since held that one chamber of a state legislature can't appeal on its own to defend a state law ([Virginia House of Delegates v. Bethune-Hill, 2019](https://www.law.cornell.edu/supremecourt/text/18-281)), and one house of Congress would likely be treated the same way. And a hostile House wouldn't try.
|
||||
- Private people can't appeal to defend a law just because they care about it ([Hollingsworth v. Perry, 2013](https://www.law.cornell.edu/supremecourt/text/12-144)). Borrowers with signed loans probably have enough at stake to defend their own loans. The federation and applicants may not.
|
||||
|
||||
**Why the bank doesn't get its own lawyers.** Some federal lenders can go to court in their own name. Here that would add little. After *Slaughter* the President controls the board, and a board that doesn't want to defend the bank won't. Going to court for the government is also an executive power, so lawyers allowed to fight the President in court would be challenged themselves. The power to enforce these rules sits with the people they protect, not with the bank.
|
||||
|
||||
### The lock doesn't depend on Washington
|
||||
|
||||
[The lock](the-lock.md) lives in each co-op's own charter and is enforced by an independent lock-holder, not by the bank. So repealing the bank's law doesn't unlock a co-op in ordinary times. There's one big exception. The lock only holds in bankruptcy if federal bankruptcy law is amended (see [When a Locked Co-op Goes Bankrupt](bankruptcy.md)), and a Congress that repeals the bank could repeal that amendment too.
|
||||
|
||||
States are the second layer. Co-op law in America is state law. Massachusetts, for example, has had a law for worker co-ops since 1982, but a co-op can leave that status by a two-thirds vote of its members ([Mass. Gen. Laws ch. 157A, § 4](https://malegislature.gov/Laws/GeneralLaws/PartI/TitleXXII/Chapter157A/Section4)). That's exactly the exit the lock closes. So the model includes a short [model state law](lock-state-law.md) that recognizes locked co-ops and their lock shares, turns off the members' exit for co-ops that chose the lock, and makes lenders' sales outside court follow the lock's rules. States adopt it or don't. Nothing in the federal law requires it. It only protects co-ops that chose the lock, in exchange for a loan they chose to take.
|
||||
|
||||
### Federal test, state copies
|
||||
|
||||
[Open Problems](open-problems.md) asked whether the bank should be federal or run by states. The answer here is both, for different jobs:
|
||||
|
||||
- **The test is federal.** One bank, one plan and one set of reviewers make one clean experiment. Fifty small ones, each designed a little differently, can't be added up into one answer.
|
||||
- **Any state can copy it.** A state can charter its own co-op lender under the same published rules. A state lender can even be given a steady base the way North Dakota's is, by holding some state money on deposit. Its results are reported separately and don't count toward the federal test in either direction. If Washington abandons the idea, the states that copied it keep running.
|
||||
|
||||
### The sector pays its own way
|
||||
|
||||
The last layer is the only one that doesn't depend on anybody's goodwill: the co-ops' own money. The [federation](federation.md)'s shared fund is built to become a lender itself, the way Italy's Coopfond has lent to co-ops for three decades out of their own profits ([Has This Been Tried?](precedents.md)). The sooner co-ops can finance each other, the less a hostile Congress can do.
|
||||
|
||||
## Why a hostile government might leave it alone
|
||||
|
||||
The Bank of North Dakota survived because the side that had fought it decided to give it a fair trial. This model tries to make that the cheaper choice for an opponent:
|
||||
|
||||
- **It's a one-time cost.** There's no yearly spending line to fight over.
|
||||
- **It shuts itself down if it fails.** An opponent who expects co-ops to fail can let [the decision rule](pilot-spec.md#the-decision-rule) prove it, and the rules above make it hard to rig that result either way.
|
||||
- **It buys no one's company.** It still spends public money, and conventional competitors and some employers will oppose it anyway. Italy's main employers' association got part of its worker-buyout program suspended by the EU ([Has This Been Tried?](precedents.md#italys-co-op-funds-coopfond-and-the-marcora-law)). But no industry loses its property.
|
||||
- **Every result is public,** so whoever ends it early has to explain why.
|
||||
|
||||
None of that stops a determined opponent. It only raises the cost of acting in bad faith.
|
||||
|
||||
The electric co-ops show the longer-term defense. If the test passes and the sector grows, it will eventually have members in most congressional districts. People who own part of something defend it. The same defenders can also lobby to keep a program running after it's stopped earning its place. That's why the pilot's growth depends on a fixed test, not on how many friends it has.
|
||||
|
||||
## What this can't protect
|
||||
|
||||
- **The bank itself.** A Congress and President who both want it gone can repeal it. Nothing here prevents that, and nothing should. A democracy has to be able to change its mind. The design only makes sure the repeal is done by law, and that it can't take the co-ops' charters with it.
|
||||
- **Money not yet lent,** which a budget bill can take back, and riders that bar spending.
|
||||
- **A board that's merely timid.** The rules stop a freeze and expose rejecting good applicants. They can't make a hostile board eager, stop it hiring timid staff, or stop it pulling back the credit officers' authority in public.
|
||||
- **A small constituency.** At pilot scale, around 400 co-ops employ perhaps several thousand people. That's nowhere near enough to defend anything politically. The electric co-ops became defenders after decades and hundreds of co-ops. Until this sector is large, durability comes from the co-ops' own charters and money, not from votes. How long it could take to get large is worked through in [After a Pass: How Big, How Fast](scale.md).
|
||||
- **Pressure outside the law.** Other banks refusing to work with co-ops, co-ops shut out of government contracts, targeted inspections. The federation and co-op-friendly lenders like credit unions are the only real answers, and they take time to build.
|
||||
- **The courts.** How far *Slaughter* reaches, whether damages follow when a later law breaks a loan contract, and whether the Supreme Court sends cases like these to the Court of Federal Claims despite the bank's own law will be settled by judges, not by this design. Lawsuits are also slow, both sides can use them strategically, and even with fees paid, the people with the most reason to sue are founding groups with the least time to wait.
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: Who Decides What's Essential
|
||||
section: Beyond Co-ops
|
||||
order: 112
|
||||
summary: A public subsidy for "essential" low-paid work invites every low-wage industry to call itself essential, so a fixed formula decides which work qualifies, the money goes to workers by the hour, and states can't use it as a reason to pay less.
|
||||
---
|
||||
|
||||
# Who Decides What's Essential
|
||||
|
||||
[The floor](the-floor.md) includes a targeted subsidy for essential low-paid work, paid to workers, not firms. Somebody has to clean the nursing home for the poor, and if the work can't pay a decent wage, the public should.
|
||||
|
||||
The danger is in the word "essential." Whoever decides which work counts gets to set part of the price of labor in that industry. Every low-wage industry will discover that it's essential, and every one has lobbyists.
|
||||
|
||||
## How fast "essential" grows
|
||||
|
||||
We've seen it happen. Early in the COVID-19 pandemic, the federal government published an advisory list of "essential critical infrastructure workers" who should keep working. Researchers at the CDC's occupational safety institute later matched it to standard industry codes. It covered industries employing about 81% to 87% of American workers, depending on how industries are counted ([Billock and others, 2022](https://pubmed.ncbi.nlm.nih.gov/35532007/)). That list was only advice about who could keep working, with no money attached, and "essential" still ended up meaning nearly everyone. A list that came with money would be under even more pressure.
|
||||
|
||||
## Who really gets the money
|
||||
|
||||
The second danger is quieter. Money aimed at workers can end up with employers.
|
||||
|
||||
The Earned Income Tax Credit tops up low earners' pay through the tax system. Because it draws more people into low-wage work, it pushes those wages down. One careful simulation, using a range of plausible assumptions about how workers and employers respond, found that each dollar spent raises workers' after-tax incomes by only about 73 cents. Employers capture much of the rest through lower wages, partly at the expense of low-wage workers who don't get the credit at all ([Rothstein, 2010](https://www.nber.org/papers/w14966)).
|
||||
|
||||
Washington, D.C., tried a more direct route. Its Early Childhood Educator Pay Equity Fund paid child care workers supplements of $10,000 to $14,000 a year, sent to the workers themselves, outside their employers' payrolls. More than 4,000 educators received them, and over $80 million went out in the first two years ([D.C. OSSE](https://osse.dc.gov/ecepayequity)). An independent study found the fund added about 219 child care educators, around 7% more than there would otherwise have been. Employers' own pay didn't change, so in those two years they didn't claw the money back by cutting wages ([Mathematica, 2024](https://www.mathematica.org/publications/two-year-labor-market-impacts-of-washington-dcs-early-childhood-educator-pay-equity-fund)). Two years is short, though, and D.C. has since switched to paying the money through employers' payrolls. D.C. also shows a limit of what follows: child care is mostly paid for by families, so the test below may leave it out.
|
||||
|
||||
Minnesota took a third route, and it gives workers a voice. In 2023 it created a board of three state officials, three employer representatives and three worker representatives, with the power to set minimum pay for nursing home workers. The board set $22.50 an hour for certified nursing assistants and at least $19 for everyone else, rising in 2027. The state pays for it through the rates Medicaid pays nursing homes, and it took effect in September 2026 after federal approval ([Minnesota Nursing Home Workforce Standards Board, 2025](http://www.dli.mn.gov/sites/default/files/pdf/nhwsb_annual_report_2025.pdf); [RMCEP](https://www.rmcep.com/success-stories/news-archive/p/item/69158/minnesotas-minimum-wage-standards-for-nursing-home-workers-in-effect-sept-10)).
|
||||
|
||||
## The model's answer
|
||||
|
||||
### A formula decides, not people
|
||||
|
||||
No official, agency or Congress picks industries by name. Work qualifies only if it passes two tests, measured from published government data:
|
||||
|
||||
1. **The public already pays for most of it.** Over the last three years, at least half of the industry's revenue came from public benefit programs like Medicaid, Medicare, veterans' care and public child care assistance. Ordinary government purchasing, like cleaning or security contracts, doesn't count. How that's measured, and which kinds of work pass today, is below.
|
||||
2. **The job pays less than most.** The typical hourly wage for the job, within that industry, is below the typical wage for all American jobs. In May 2025 that was about $24.50 an hour ($50,980 a year). Home health and personal care aides earned a typical $17.21 an hour, and nursing assistants $20.32 ([BLS, home health and personal care aides](https://www.bls.gov/oes/2025/may/oes311120.htm); [BLS, nursing assistants](https://www.bls.gov/oes/2025/may/oes311131.htm)). The test is by job, not by industry, so the nurses and managers in a nursing home don't pull the aides' number up, and splitting staff into separate companies doesn't change anything.
|
||||
|
||||
The first test is the important one, and it's really a test of who pays, not of what's "essential." That's on purpose: it's the one test lobbying can't argue its way around. Where the public is already the main buyer, the public is already setting the price, and a low public rate is a choice about what these workers are paid. It also keeps the program narrow. Retail, restaurants and other low-wage work paid for by private customers don't qualify. They're covered by the parts of the floor that apply to everyone, like the minimum wage and tax credits. Averaging over three years means an industry can't tip itself over the line in a single year.
|
||||
|
||||
The national statistics agencies apply the formula every three years and publish the list, with old industry codes mapped to new ones when the codes are revised. Congress can change the formula, but only for everyone at once, with a published cost estimate. It can't add one industry by name.
|
||||
|
||||
### Measuring who pays
|
||||
|
||||
The first test needs data on who pays each industry's bills. That data exists for some kinds of care, is misleading for others, and is missing for the rest.
|
||||
|
||||
- **The Census Bureau's yearly survey of service businesses** splits health care industries' revenue by who paid it ([Service Annual Survey, Table 4, 2022 release](https://www.census.gov/data/tables/2022/econ/services/sas-naics.html)). But it counts payments from private plans that run Medicare and Medicaid for the government, like Medicare Advantage and Medicaid managed care, as "private health insurance." In 2021, 24 states paid for Medicaid long-term care through those plans ([MACPAC](https://www.macpac.gov/subtopic/managed-long-term-services-and-supports/)), so the survey undercounts public money.
|
||||
- **The federal health spending accounts** count those payments as Medicare and Medicaid, where they belong ([CMS methodology](https://www.cms.gov/Research-Statistics-Data-and-Systems/Statistics-Trends-and-Reports/NationalHealthExpendData/Downloads/dsm-17.pdf)). But they're organized by type of care, not by industry, and some categories mix several industries.
|
||||
- **For social assistance, like child care and non-medical home care, the Census survey doesn't ask who pays at all.**
|
||||
|
||||
**What the two health sources show.** The survey column counts only payments made directly by Medicare, Medicaid and other government programs, so it's a floor. The spending accounts column counts managed care too, but its categories are broader.
|
||||
|
||||
| Kind of work | Census survey, 2020–22 average | Health spending accounts, 2021 |
|
||||
| --- | --- | --- |
|
||||
| Home health care | About 63% | About 72% |
|
||||
| Nursing homes | About 66% | About 56%, combined with retirement communities |
|
||||
| Residential care for developmental disabilities, mental health and addiction | About 67%, but only 45% from Medicare and Medicaid. The rest is other government programs, mostly state ones | Part of a mixed category that's about 61% public |
|
||||
| Assisted living and retirement communities | About 16% (2020–21; 2022 withheld). Families pay nearly half | Combined with nursing homes, above |
|
||||
| Hospitals | About 34% (2020–21; part of 2022 withheld) | About 52% |
|
||||
| Child care | Not measured | Not covered |
|
||||
|
||||
Sources: Census [Service Annual Survey, Table 4](https://www.census.gov/data/tables/2022/econ/services/sas-naics.html), 2022 release; [MACPAC, from the national health spending accounts](https://www.macpac.gov/wp-content/uploads/2023/12/EXHIBIT-3.-National-Health-Expenditures-by-Type-and-Payer-2021.pdf), counting Medicare, Medicaid, CHIP and veterans' and military care.
|
||||
|
||||
Three things stand out:
|
||||
|
||||
- **Home health passes on any measure.** Non-medical home care, where most personal care aides work, isn't measured by industry at all. Medicaid paid about 60% of the broader category that includes most home and community-based care in 2023 ([MACPAC](https://www.macpac.gov/medicaid-101/spending/)), so it very likely passes, but real data has to show it.
|
||||
- **Counting managed care properly puts hospitals near the line.** Because the wage test is by job, a hospital that qualifies brings in its low-paid aides, housekeepers and cooks. That's consistent with the rule, since the public does pay for most hospital care, but it would add a great deal to the cost. It's shown here so nobody is surprised by it later.
|
||||
- **Child care is the hard case.** The US Treasury estimated that federal and state spending equaled about 47% of child care businesses' revenue in 2019, and said that probably overstates the public share, since some of that money pays for care outside those businesses ([US Treasury, 2021](https://home.treasury.gov/system/files/136/The-Economics-of-Childcare-Supply-09-14-final.pdf)). Pandemic relief money pushed the share up for a few years. Until there's real data, child care's status is undetermined.
|
||||
|
||||
**So the law fixes the method in advance:**
|
||||
|
||||
- **Which money counts.** A closed list: Medicare and Medicaid, including their private plans; CHIP; veterans' and military health care; the Indian Health Service; state programs for people with disabilities; child care assistance and Head Start. Only money paid to businesses in the industry counts. Workers' compensation, tax credits, Social Security benefits paid on a resident's behalf (which are the resident's own income), ordinary government contracts, and temporary emergency money don't. Changing the list follows the same rule as changing the formula: for everyone at once.
|
||||
- **Measured from payment records.** Public money comes from the programs' own payment records, matched to businesses by tax ID, not from what businesses say in a survey. Revenue comes from the Census survey, counting revenue from care, not donations or investment income. Where the Census withholds a figure, the last published one is used, and flagged.
|
||||
- **New data, on a deadline.** The Census survey has three years to report Medicare and Medicaid plan payments separately from private insurance, and to ask child care and home care businesses who pays them. The cost comes out of the supplement's own budget.
|
||||
- **Status is sticky.** An industry joins the list only after two reviews in a row at 50% or more, and leaves only after two reviews in a row below 45%. So sampling noise near the line, or one year's emergency money, can't flip it. A newly qualifying industry is paid only after its cost estimate has been public for a full budget year.
|
||||
- **No losing status for missing data.** If a list isn't published on time, the last one stays in force. When industry codes are revised, an industry keeps its status for two reviews while the new codes are mapped, and the mapping is published for comment first.
|
||||
- **No moving into a qualifying industry.** A company that works mainly for a related business takes that business's industry, and industry codes are checked against state licensing. An assisted living operator can't qualify by moving its aides into an affiliated home health agency.
|
||||
|
||||
### Paid to workers, by the hour
|
||||
|
||||
- **Straight to the worker,** as a fixed amount per hour worked, as D.C. did at first.
|
||||
- **Real hours only.** Federal payroll forms report pay, not hours. So hours come from records that already exist for public programs, like the electronic visit records federal law has required for Medicaid home care since 2020 ([Medicaid.gov](https://www.medicaid.gov/medicaid/home-community-based-services/guidance/electronic-visit-verification-evv)), and from employers' own payroll hours where those aren't available. Hours are capped at 2,600 a year per worker, about 50 a week, across all their jobs. Live-in workers are paid for the hours their state program or wage law already counts as work. Samples are audited, and when padded hours are found, the employer repays.
|
||||
- **Everyone doing the work counts,** including home care workers employed directly by the people they care for through a state program, and self-employed providers paid by a public program. Their hours come from the program's own records.
|
||||
- **How much.** A first draft: $3 an hour, set in law. That's enough to take a typical home care aide from $17.21 to about $20.20, roughly what a typical nursing assistant earns now, and an aide in the bottom tenth from $13.00 to $16.00 ([BLS](https://www.bls.gov/oes/2025/may/oes311120.htm)), if employers don't claw any of it back (see [below](#stopping-the-money-from-leaking)). It's a budget choice, not a living wage. It rises each year with average wages, not prices, so it doesn't shrink against the pay it tops up. Every three-year review publishes vacancies, turnover and pay in qualifying work next to the amount, so Congress can see whether it's doing anything.
|
||||
- **The same amount for everyone in a qualifying job.** An earlier draft faded it out as a worker's own pay rose. That would have taken a cut of every raise, including raises a state or wage board ordered, and pay in these jobs is bunched too tightly for it to save much: nine in ten home care aides earn under $21.65, and nine in ten nursing assistants under $25 ([BLS](https://www.bls.gov/oes/2025/may/oes311131.htm)). It would also have needed an hourly rate for every worker, which doesn't exist for live-in, day-rate or self-employed providers. So the only way off is the job-level test above: when typical pay in the job rises past the national median, the job leaves the list.
|
||||
- **How it's taxed.** It's paid like wages: income tax, payroll tax and Social Security credit, and it counts as earned income for the Earned Income Tax Credit, as any other pay does. A parent in the part of that credit that shrinks with income keeps roughly 60 cents of each dollar after all of it. But the law excludes the supplement from income when deciding eligibility for Medicaid, CHIP, marketplace health subsidies, food assistance, SSI and housing help, so it can't knock anyone off them. That takes a line in each program's rules, and it costs money that hasn't been estimated.
|
||||
- **Who can get it.** Anyone allowed to work in the US at the time, with their own Social Security number. Authorization is checked each quarter against Social Security and immigration records. A mismatch, like a name change, gets 90 days to fix before any payment is held, and hours worked while authorized are always paid. That needs saying in the law itself. Federal law bars immigrants who aren't "qualified aliens" from "any grant" or "similar benefit" paid with federal money ([8 U.S.C. 1611](https://www.law.cornell.edu/uscode/text/8/1611)), and that leaves out many people who are allowed to work, like those with Temporary Protected Status or DACA. So the supplement's law says it applies to anyone authorized to work, notwithstanding that rule, and that states can't exclude them either. That's a choice Congress could split off from the rest. The law also says the supplement isn't a public benefit for immigration purposes, so families don't avoid it out of fear.
|
||||
- **Same for everyone.** Co-ops and conventional firms qualify on exactly the same terms. The subsidy doesn't favor co-ops, and it doesn't skew [the pilot's](pilot-spec.md) comparison with conventional firms, because both sides get it.
|
||||
|
||||
### Stopping the money from leaking
|
||||
|
||||
Money aimed at workers has three ways out. Each gets its own rule.
|
||||
|
||||
- **The public payer.** The biggest risk is that states cut their Medicaid rates, knowing the federal supplement will fill the gap. So a state's rates for qualifying care can't fall behind inflation. If they do, the federal share of the state's Medicaid spending on that care drops by the same amount. The supplement also can't count toward any rule requiring a share of rates to go to wages, like [Medicaid's 80% rule for home care](high-turnover.md#when-medicaid-decides-how-much-goes-to-pay), and it's left out when rates are set.
|
||||
- **The employer.** An employer can't let any job's total pay, wages plus benefits, fall behind inflation compared with the year before its workers qualified. One that does repays the difference to its workers, with a penalty. Pay in qualifying work is compared every year with similar work that doesn't qualify, so slower raises show up. And the supplement can't be counted toward any minimum wage, and a state or wage board can't set its minimums lower because of it.
|
||||
- **The owner.** In industries full of related-party rent and management fees, public money that frees up payroll can still end up with owners. The supplement doesn't fix that. It's a reason for the rate rules states set, and for co-ops.
|
||||
|
||||
### The budget
|
||||
|
||||
The supplement is a fixed amount per hour, set in law and rising with average wages, so it doesn't shrink when Congress wants to starve it quietly. The cost is whatever that amount times the qualifying hours comes to, published every year. When the formula adds an industry, the cost rises, and everyone can see by how much.
|
||||
|
||||
For scale, in May 2025 employers reported about 4.4 million home care aide and nursing assistant jobs in industries likely to qualify: about 2.3 million in non-medical home care and similar services, 1.1 million in home health, 590,000 in nursing homes and 330,000 in residential care for people with disabilities or mental illness ([BLS, aides](https://www.bls.gov/oes/2025/may/oes311120.htm); [BLS, nursing assistants](https://www.bls.gov/oes/2025/may/oes311131.htm)). Those are jobs, not people, since some people hold two. At $3 an hour and 1,200 to 1,500 hours a year each, that's about $16 billion to $20 billion a year. Not included: aides hired directly by the people they care for, who don't all show up in these counts; self-employed providers; about 510,000 hospital aides and nursing assistants, plus hospital housekeepers and cooks, if hospitals qualify; and the cost of leaving the supplement out of benefit tests. About 430,000 more jobs are in assisted living, which doesn't qualify on today's numbers. Each dollar an hour on the amount adds about a third.
|
||||
|
||||
### Worker voice, too
|
||||
|
||||
A formula keeps lobbyists out, but it also leaves workers out. Minnesota's board shows another way: workers, employers and the state set minimum pay together, and the public rates pay for it. The two fit together. States can set up boards like Minnesota's for qualifying work, and the federal supplement comes on top, because states can't cut their own spending.
|
||||
|
||||
### A way off the list
|
||||
|
||||
The founding plan pairs this subsidy with public research on automating the same work, and says plainly that it's meant as a bridge, not forever ([Ownership and the Floor](the-floor.md)). The formula makes the exit automatic. The wage test is measured without the supplement, so the supplement itself can't push an industry off the list. When real pay rises, because of automation, higher public rates or anything else, the industry drops off, fading out over two reviews rather than all at once. For care work, the bridge will probably be long. It ends when the public pays what the work is worth.
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **Some essential work won't qualify.** Where families, not the public, pay most of the cost, the formula leaves the work out. That may include a lot of child care, where D.C.'s evidence comes from. Families who pay for care themselves, and people who care for relatives unpaid, get nothing from this. That's the price of a rule that can't be bent. States can still do what D.C. did.
|
||||
- **The formula will be fought over.** Industries will argue about how their work is classified and what counts as public revenue. Checking workers against public program records, not just the codes employers pick, is the defense. It isn't perfect.
|
||||
- **Capture isn't ruled out.** The rules catch pay cuts, including cuts in benefits, and states cutting their rates. They can't catch every raise that would have happened anyway.
|
||||
- **It leaves out workers who aren't allowed to work here.** That's a political choice, not something the law forces: paying people for work they can't legally be hired for would be hard to pass, and this page doesn't pretend otherwise. It's worse than leaving them out. By the logic of the tax-credit research above, a supplement that draws more authorized workers into care work can hold down the wage employers pay, and workers who don't get the supplement bear that with nothing to make up for it. Many care workers are immigrants, and undocumented ones are more likely to work off the books, where no program reaches them ([PHI](https://www.phinational.org/policy-research/key-facts-faq/)). What protects them is the rule against employers cutting a job's pay, and minimum pay set by state wage boards like Minnesota's, which covers every worker in the job whatever their status.
|
||||
- **It costs real money.** Tens of billions of dollars a year is plausible once home care and nursing homes qualify, and much more if hospitals do. That's a choice about what care workers are paid, and the page states it so voters can make it.
|
||||
- **It's federal money for work states largely pay for.** States already can, and some do, raise care workers' pay themselves. The federal role here is to make that pay not depend on where you live, and to stop states using it as a reason to pay less. That sits uneasily with leaving child care out: its workers are overwhelmingly women, and if it doesn't qualify, their pay still depends on their state.
|
||||
- **Congress can still change the formula.** It's harder to bend than a list, not impossible.
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: The Federation
|
||||
section: Growing the Sector
|
||||
order: 70
|
||||
summary: Co-ops pool risk through a shared fund that absorbs shocks, moves workers when a firm fails, and becomes a lender in its own right.
|
||||
---
|
||||
|
||||
# The Federation
|
||||
|
||||
Worker ownership has a risk problem that's easy to miss: **concentration.** It matters even more in a young, voluntary co-op sector.
|
||||
|
||||
A worker-owner holds their salary, their profit share, and their skills all inside one firm. That is exactly the risk a shareholder diversifies away and a worker cannot. It is the real source of risk-aversion: if one bad bet can kill the company that is also your paycheck and your equity, you take the safe option every time, and an economy of safe options stops producing breakthroughs.
|
||||
|
||||
The fix is a structure between firms, and Mondragon already built it:
|
||||
|
||||
- **Firms pool a slice of profits into a mutual solidarity fund.** The fund absorbs shocks. A firm having a bad year draws support instead of dying.
|
||||
- **When a firm does die, the federation redeploys its workers into member firms.** Your company failing stops meaning your life failing. *This* is what licenses healthy risk-taking: not lower stakes on paper, but a floor under the fall.
|
||||
- **The fund becomes a second lender.** Over time, the federation (financed by its members and by the indivisible reserves of dissolved co-ops) becomes a source of patient capital that doesn't depend on the public bank at all. That is the path to the co-op sector outliving the political coalition that started it.
|
||||
- **Federations can insure their members' savings.** A federation can run its own savings insurance pool, as long as it meets the national standards and reinsures with the others. Its co-ops' workers are then covered close to home (see [Protecting Members' Savings](savings-guarantee.md)).
|
||||
|
||||
Membership is voluntary, but the incentives all point in: unfederated co-ops face the concentration problem alone.
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: The Founder Problem
|
||||
section: The Co-op
|
||||
order: 40
|
||||
summary: Without the chance to sell the company, why would a strong founder start a co-op? The numbers, the tools, and the honest answer.
|
||||
---
|
||||
|
||||
# The Founder Problem
|
||||
|
||||
This is the hardest part of the design, and it needs to be said plainly.
|
||||
|
||||
**In a conventional business, a founder who succeeds gets paid in two ways: a share of the profits every year, and a big payday when the company is sold.** A co-op founder gets neither in full. The profits are shared with every member, and [the lock](the-lock.md) means the company can never be sold. So the obvious risk is *adverse selection*, meaning the pool you get is the one nobody else wanted: the best entrepreneurs go conventional, and the bank's applicants are the people who couldn't raise money anywhere else. If that happens, the experiment is rigged to fail before it starts.
|
||||
|
||||
This page runs the numbers, lays out the tools, and says honestly what they can and can't fix.
|
||||
|
||||
## The numbers
|
||||
|
||||
Take the same business, with five founders who all draw normal salaries, built two ways:
|
||||
|
||||
- **As a conventional firm:** the five founders own it outright, financed by a bank loan. Each year they pay out 60% of profit to themselves and keep 40% in the business to fund growth. At year 15 they sell it for three to six years' profit.
|
||||
- **As a co-op:** the five founders are the first members. The profit split is the same (60% to members, 40% kept in the reserve and the federation fund), but it's shared by everyone who joins, and nobody sells. (See [Member Capital Accounts](capital-accounts.md) for how the money flows.)
|
||||
|
||||
Here's what **one founder** has received by year 15 if the business succeeds, on top of salary. Money received later counts for less than money now. It's discounted at 5% a year, the rate used in the founder study below.
|
||||
|
||||
| Kind of business | Profit per worker (after salaries) | Size at year 15 | **Co-op founder** | **Conventional founder** (one-fifth owner) |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Care work | $3,000 | 40 people | about $19,000 | $140,000 to $180,000 |
|
||||
| Main Street | $15,000 | 40 people | about $96,000 | $710,000 to $880,000 |
|
||||
| Strong | $40,000 | 80 people | about $260,000 | $3.2 million to $4.2 million |
|
||||
| Very high margin | $150,000 | 200 people | about $980,000 | $29 million to $38 million |
|
||||
|
||||
**In the success case, a co-op founder ends up with somewhere between about a seventh and a fortieth of what the owner of the same firm would.** The bigger and more profitable the firm, the wider the gap. Nothing on this page makes it go away.
|
||||
|
||||
Three caveats about these numbers:
|
||||
|
||||
- **They assume the firm exists and works equally well either way.** That's the big assumption. If co-op ownership makes firms less productive, or if the firm would never have been founded as a co-op, the comparison is moot. The first is exactly what [the pilot](pilot-spec.md) measures. The second is the founder problem itself.
|
||||
- **They're a simple model, not a forecast.** Founders start at a full share. New hires join as members after three months and ramp up to a full share over five. The part of a share they don't get yet goes to the reserve, as tax law requires, not to the founders ([How a Locked Co-op Is Taxed](taxes.md#the-tenure-ramp)). An earlier version of this table let it go to the other members, and showed co-op founders with roughly twice as much. Payroll tax on the founding share is left out too. The workforce grows steadily to the size shown. Change the growth rate or the sale price and the ratios move, but the gap never comes close to closing.
|
||||
- **Taxes are left out on both sides.** Counting them would widen the gap, not close it. The owner of a partnership or S corporation can usually deduct up to 20% of the business's profit, which a co-op member can't ([How a Locked Co-op Is Taxed](taxes.md#how-this-compares-with-other-businesses)). In most industries, a founder who sells shares in an ordinary small corporation after five years can leave up to $15 million of the gain untaxed, or ten times what they put in if that's more ([26 U.S.C. § 1202](https://www.law.cornell.edu/uscode/text/26/1202)), and a co-op founder has no sale to leave untaxed.
|
||||
|
||||
## Where the money went
|
||||
|
||||
In the very-high-margin case, a worker who joined in year three has received about $600,000 by year 15 (see [Member Capital Accounts](capital-accounts.md#what-it-adds-up-to)). The people hired after them have less, because they've had fewer years at full share, but all 200 are building a share of what the business earns.
|
||||
|
||||
**If the firm exists either way, the founder's "missing" money isn't lost. It went to the people who did the work.** That's the whole model. A version of the co-op that paid founders like owners would just be a conventional firm with extra steps.
|
||||
|
||||
So the real question isn't "can a co-op match a conventional exit?" It can't. The question is: **are there enough good founders for whom the co-op deal is good enough?**
|
||||
|
||||
## Why it's less lopsided for most founders
|
||||
|
||||
The table compares a co-op with a conventional firm *that succeeds*. Most don't. And for a founder without much money, a big risky payoff is worth far less than its average.
|
||||
|
||||
- **Most venture-backed founders get nothing from the exit.** Robert Hall and Susan Woodward studied nearly every venture-capital-backed US startup from 1987 to 2008, about 22,000 companies. The average founder got $5.8 million at exit, but **almost three-quarters got nothing at all**. A few enormous wins drive the average. For a founder with $100,000 in savings and an ordinary aversion to risk, they calculated that the chance to found a venture-backed startup was worth *slightly less than nothing* compared with a salaried job ([Hall and Woodward, *American Economic Review*, 2010](https://web.stanford.edu/~rehall/HallWoodward6.pdf)). That study covers tech and biotech startups, not the small businesses the bank would fund, but the logic carries over. A long-shot payday is worth much less than its average to someone who can't afford to lose.
|
||||
- **Most small-business owners earn less than they would as employees.** Barton Hamilton found that after ten years in business, the median self-employed person earned 35% less than in a paid job. That counts the growth in value of their business ([Hamilton, *Journal of Political Economy*, 2000](https://www.journals.uchicago.edu/doi/abs/10.1086/262131)). The median small business that does sell goes for about $345,000 ([BizBuySell, 2024](https://smallbiztrends.com/bizbuysell-insights-report-q4-2024/)). Hamilton read the gap as people paying to be their own boss, and co-op founders give up some of that, since a co-op founder is one vote among many.
|
||||
- **The co-op's downside is smaller.** A co-op that fails costs its members their jobs and what's left in their accounts. Under [the pilot's loan terms](pilot-spec.md#loan-terms), members give no personal guarantees, while SBA loans require one from every owner of 20% or more. That's a feature of how the bank lends, not of co-ops as such. It still matters to the founder deciding which to start.
|
||||
|
||||
So for a founder *without* family money, the co-op isn't simply a worse lottery ticket. It's a different bet: a much smaller top prize, a lower chance of losing everything, and a salary throughout.
|
||||
|
||||
**Where the co-op really does lose is at the top:** the founder who is confident, wealthy enough to absorb a failure, and building something with very high profit per worker. That founder will usually go conventional, and the model should expect them to.
|
||||
|
||||
## The tools
|
||||
|
||||
These are the ways a co-op can reward founders without selling the firm. Each is limited so it can't turn a founder back into an owner, or let founders pay themselves ahead of the bank.
|
||||
|
||||
- **Member capital accounts.** A founder builds up an account like every other member, from year one. See [Member Capital Accounts](capital-accounts.md).
|
||||
- **A founding share.** For the first ten years, 10% of the members' profit pool goes to the people who were there at the start. It's split by the hours each person worked in the first three years, counting everyone on the payroll, members or not, so the first hires share it with the founders and keeping early workers out of membership doesn't help. It's paid only to people still working at the co-op that year. When someone leaves, their part goes back to the pool. The reserve's and the federation's shares are figured before it, so it comes out of the members' side only, and nobody's founding share in a year can be more than the going-rate pay for their job. Because it goes to some workers and not others, it's paid as pay, not as a profit share ([How a Locked Co-op Is Taxed](taxes.md#the-founding-share)). So the reserve takes 30% of it, like any pay above the going rate ([Investing for the Long Run](the-long-run.md#the-rules)), and it counts in the pay ratio. In the table it's about a tenth of what a founder receives. It is a claim on the work of people hired later, which is what the thesis objects to. The limits keep it small: ten years, a fixed slice, and only while you still work there. A fixed slice means founders gain nothing from keeping the co-op small later on. They would gain a little from hiring late in the first three years, since fewer early hours means a bigger share of the slice for each.
|
||||
- **Back pay for unpaid early work.** If founders take reduced pay before the business has revenue, the difference is recorded as a debt the co-op owes them. Pay can't go below the minimum wage once the co-op exists ([Owners Are Still Workers](labor-law.md#founders-unpaid-work)). The limits:
|
||||
- It's valued at no more than the pay the co-op sets for the same job once it has revenue.
|
||||
- It earns interest at the Treasury rate.
|
||||
- It ranks behind the bank, can't be repaid from the bank's loan money, and isn't repaid while any bank payment is late.
|
||||
- **Founder money.** Cash a founder puts in comes in one of two forms. It can be a loan at no more than the Treasury rate plus three points. Or it can be the same voteless, expiring investment any outside investor could make under [Capital, After](capital.md). Either way it ranks behind the bank. A founder who remortgaged their house to start the co-op can take the investor's terms, which reward that risk.
|
||||
- **Founder salary premiums.** A founder who is also the chief executive can be paid like one. The founding bylaws can set that pay for at most the first five years. After that, the members set it, like every other salary.
|
||||
- **No founder vetoes.** Founding bylaws can't give founders extra votes, vetoes, or rules requiring a supermajority to remove them. Founders write the first bylaws before any other worker exists, so this is the one place the model protects future members from them.
|
||||
|
||||
**What's deliberately not here: a founder's tail.** One idea considered was letting founders keep a voteless share of profits for some years after they leave, as a stand-in for the sale price. It was left out. It would be a claim on the work of people the founder no longer works alongside, which is exactly what [the thesis](thesis.md) says this kind of firm exists to prevent. In the largest case it would also have paid each departed founder several times what a working member earns.
|
||||
|
||||
## The honest bottom line
|
||||
|
||||
None of this replaces a large exit. The model is betting on three kinds of founders:
|
||||
|
||||
1. **People without wealth** who would never risk a conventional startup. For them, a salaried, lower-risk path to building something may be the best deal available.
|
||||
2. **Groups of workers founding together.** When the "founders" are the first ten employees, the gap between founder and worker mostly disappears.
|
||||
3. **People who value what the co-op offers**: control, stability, and a firm that can't be sold out from under them.
|
||||
|
||||
Whether there are enough of them is an empirical question. [The pilot](pilot-spec.md) publishes the founders' backgrounds, including their previous earnings where the data allows, so that if the strongest founders stay away, it shows. If the portfolio fails, the first question will be "was it co-ops, or was it who applied?" [Co-ops or Their Founders?](who-founds.md) sets out how the pilot answers it: founders on both sides are measured from government records, and the verdict has to say how much their backgrounds explain. It doesn't change pass or fail. If the lock keeps strong founders away, that's part of what the lock does.
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Does Sharing the Profit Mean Less Effort?
|
||||
section: The Co-op
|
||||
order: 21
|
||||
summary: In a big co-op, your extra effort barely moves your own profit share, so the model leans on what the evidence points to (small teams, open books, team bonuses the members design, trust) and sets a fair, fast process for the rare member who won't do the work, with protections so it can't be turned on the wrong people.
|
||||
---
|
||||
|
||||
# Does Sharing the Profit Mean Less Effort?
|
||||
|
||||
Here's the oldest objection to shared ownership. In a co-op of 1,000 members, if you work hard enough to add $1,000 to the profit, your own share goes up by about a dollar, less after the reserve. Everyone else's goes up by a dollar too, including anyone who didn't bother. So why work harder? Economists call it the free-rider problem, or the 1/N problem: with N members, you get 1/N of what you add.
|
||||
|
||||
The founding plan's answer is culture and structure: co-ops hold members to their share of the work, and groups of co-ops keep each unit small enough that people notice. This page checks that against the evidence and turns it into rules.
|
||||
|
||||
## What the evidence says
|
||||
|
||||
**Workers can tell who isn't pulling their weight, and many act on it.** A large study surveyed workers at over 300 worksites in 14 companies with group incentive pay, like profit sharing or employee stock, and added questions to a national survey. These were conventional companies, not co-ops. Most workers said they could tell when a co-worker was shirking. Many said they would talk to that person or report it, and many said they had. Acting on it was more common where workers had a real stake in the results, in smaller workplaces, and where relations with management were good. Workplaces where more workers acted also reported that co-workers worked harder and the workplace worked better ([Freeman, Kruse and Blasi, 2008](https://www.nber.org/papers/w14227)). That's a link, not proof of cause: better-run workplaces may simply produce both.
|
||||
|
||||
Three details matter for this model:
|
||||
|
||||
- **Group incentives went with other things:** teams, training, varied work, light supervision and decent base pay. On their own, they did less.
|
||||
- **Size mattered, and so did trust.** The link was strongest in the smallest workplaces and weaker in the largest. But it held in larger companies where workers trusted management. The authors' reading: workers act to raise output only when they're confident they'll share in the gains.
|
||||
- **A small stake can backfire.** Where the profit-sharing bonus was small, eligible workers were *less* likely than others to act against a shirker. The authors think a shared bonus makes people fear co-workers will resent them for speaking up, and a small bonus isn't worth that. In a co-op of 1,000, everyone's slice of a single person's effort is small.
|
||||
|
||||
**Ownership doesn't seem to hurt performance.** A review of 102 studies covering nearly 57,000 firms found employee ownership had a positive but very small link with firm performance (a correlation of 0.04), with no clear difference by firm size ([O'Boyle, Patel and Gonzalez-Mulé, 2016](https://doi.org/10.1111/1748-8583.12115), as summarized by [LEP](https://www.economicpossibility.org/sources/employee-ownership-and-firm-performance-a-meta-analysis)). That covers employee ownership in general, not co-ops specifically. In France, worker co-ops were as productive as conventional firms or more so ([Fakhfakh, Pérotin and Gago, 2012](https://ideas.repec.org/a/sae/ilrrev/v65y2012i4p847-879.html)), though that's co-ops that survived, under French law.
|
||||
|
||||
**What the evidence can't settle.** None of it is an experiment, none of it studied co-ops of thousands of members governed one member, one vote, and there's no direct evidence on the dismissal process below. So these rules are a starting point, and the pilot measures whether they work.
|
||||
|
||||
## Required, and up to each co-op
|
||||
|
||||
Governance is up to each co-op ([Worker Ownership](worker-ownership.md)). So this page separates two things:
|
||||
|
||||
- **Required** in every locked co-op's charter: the protections in the dismissal process, protections for disability, pregnancy and leave, the rules on bonuses, and the reporting.
|
||||
- **Standard bylaws,** which a co-op can change by vote: how teams are organized, how often the books are shared, and who hears a dismissal first.
|
||||
|
||||
## Small teams, open books
|
||||
|
||||
People notice what their co-workers do in teams, not in firms of thousands. The standard bylaws organize work in teams small enough that everyone knows what everyone else does, and give each team its own numbers: what it produced, what it cost, how much work had to be redone.
|
||||
|
||||
Where a line of business gets big enough to be its own firm, it can become one, as Mondragon's co-ops did ([When Growing Means Sharing](growth.md)).
|
||||
|
||||
The standard bylaws share results with every member every quarter, or every month once a co-op has more than 50 workers, and offer training in reading them. The 50 is a judgment, not a finding. In a smaller co-op, most people can see most of the work themselves, and closing the books every month costs bookkeeping time a small co-op may not have. A team can still get its own numbers monthly at any size if the members want. The [federation](federation.md) supplies templates and training, so small co-ops don't have to build their own.
|
||||
|
||||
## Team bonuses the members design
|
||||
|
||||
The profit share is split equally per hour worked, and that doesn't change. But pay can include bonuses ([Who Sets the Pay](pay.md)), and a co-op can pay team bonuses tied to results the team can see and affect. The study found that the bigger the gain-sharing bonus, the more workers acted against shirking.
|
||||
|
||||
The required rules keep bonuses from becoming a way to reward friends or push people too hard:
|
||||
|
||||
- **Every bonus follows a formula the members approved in advance,** in writing, before the year starts. Managers apply it. They don't pick who gets what. That covers any bonus a manager hands out under [Who Sets the Pay](pay.md). Managers still decide raises within the bands.
|
||||
- **Team bonuses, not individual ones.** The study found that bigger individual bonuses went with *less* action against shirking, perhaps because they make co-workers competitors. So a formula pays teams, and a team's bonus is split among its members by hours worked. Managers' own results pay, set by the board, is a separate formula under the same limits and is published by name.
|
||||
- **The total is limited.** All bonuses together can't be more than 10% of the co-op's total pay for the year, and no one's bonus can be more than 15% of their own pay. That stops one small team, or the managers, from taking the whole pot. The study doesn't set these numbers, and it doesn't argue for a limit at all. Its finding runs the other way: bigger group bonuses went with more action against shirking. The limit is a policy choice, to keep bonuses from turning into piece rates that push people too hard, or into a way to pay friends. For scale, workers in the study who got profit sharing or gain sharing received a most recent bonus worth about 5% of their pay at the median (4.6% in the national survey, 5.7% in the companies) and 9–12% on average. But that counted profit sharing, and a member's equal profit share already plays that part here. The bonus comes on top.
|
||||
- **Paid from a profit, or from the savings themselves.** A bonus tied to the co-op's results is paid only out of a profit on the audited books, and takes at most half of the year's profit before bonuses. A gain-sharing bonus tied to a team's own measured savings (less waste, fewer injuries, less redone work, against a baseline set before the year) can be paid even in a year with no profit, but only up to half of the savings and 3% of the team's pay. If the books are later restated, bonuses paid on the wrong figures are taken back from future bonuses.
|
||||
- **Measure quality and safety, not just speed:** work that had to be redone, waste, injuries. Customer ratings can't be used unless they're checked every year for bias by race, gender, age and disability, because customers can rate some workers lower for the same work.
|
||||
- **Measured per hour, with protected leave left out.** A teammate's parental leave, medical leave or accommodated schedule can't lower the team's numbers.
|
||||
- **How work and costs are split between teams is published,** and a team can challenge its numbers.
|
||||
- **Reported.** Bonuses are reported in the co-op's pay-gap figures by gender, race, age and disability.
|
||||
|
||||
Bonuses still stay within the approved pay bands and count toward the pay ratio. Bonuses that push pay above the going rate give the reserve its share ([Investing for the Long Run](the-long-run.md)).
|
||||
|
||||
Managers need a reason to deal with problems too. The board can tie part of managers' pay, within their bands and the limits above, to how their units do, and the board, not the teams they manage, reviews them.
|
||||
|
||||
## When a member won't do the work
|
||||
|
||||
Holding members to their share of the work has to mean something specific. Otherwise it means nothing, or it means removing whoever a majority dislikes. So every co-op's charter includes this process.
|
||||
|
||||
**Help first.** Before any warning about performance, the co-op checks for a cause it has to accommodate, like a disability, pregnancy or caregiving leave, and offers a reasonable change. Performance is judged against the accommodated schedule. Then comes a written plan to improve, with training or a move to other work if that fits. These protections apply however small the co-op is, even below the size where federal discrimination law starts.
|
||||
|
||||
**Then a fair, fast decision.**
|
||||
|
||||
- **Written charges and the evidence,** at least 10 days before the hearing.
|
||||
- **A hearing** before a panel of five members drawn by lot, no more than a week before the hearing. The draw leaves out whoever raised the complaint, the person's managers, close family or partners of either side, and anyone on the person's team now or six months before the charges, so moving people between teams doesn't change who can sit. Either side can strike one name, and either side can challenge a panelist for a close tie or open hostility. Struck names are redrawn. **The co-op needs four of the five votes.**
|
||||
- **In a small co-op,** there may not be five members left who aren't involved, and the rest may all be close to one side. So in a co-op of fewer than 15 members, or any co-op that can't fill the panel, the [federation](federation.md) draws the five from members of other co-ops, under the same rules. A person in a co-op of any size can ask for that outside panel.
|
||||
- **Paid, private and recorded.** Panel time is paid work time. The federation pays outside panelists' time and travel from its dues. Nobody on either side can contact a panelist privately. The hearing is recorded and closed, and the panel sees only what an accommodation requires, not medical records. Interpreters and disability accommodations are provided.
|
||||
- **The person can bring a co-worker or union representative.**
|
||||
- **Just cause, proved by the co-op.** The co-op has to show it's more likely than not.
|
||||
- **A written decision within 30 days** of the charges.
|
||||
- **An outside arbitrator on request,** from a list the federation keeps, on appeal. The arbitrator works from the recording and can hear new evidence if the person had no representative or interpreter. If the dismissal is overturned, the person gets their job back with back pay.
|
||||
- **Serious misconduct,** like theft or violence, allows suspension with pay while the hearing happens.
|
||||
|
||||
Co-ops that hire managers can let them dismiss members directly. The member can still take it to the hearing and the arbitrator, and gets their job back with back pay if it's overturned.
|
||||
|
||||
**Protections that can't be waived.**
|
||||
|
||||
- **No retaliation.** If a member is dismissed within 12 months of organizing, filing a complaint, reporting a safety problem, running for the board or leading opposition in a vote, the co-op has to prove the dismissal had nothing to do with it.
|
||||
- **Quitting under pressure counts.** A member who quits within 60 days after a transfer, a cut in hours or pay, a worse schedule, losing their regular clients, or being left out of a bonus can ask for a hearing, and the quit is treated as a dismissal. That's the same test [When People Don't Stay Long](high-turnover.md#no-pushing-people-out-before-theyd-join) uses for workers on probation.
|
||||
- **Probation isn't a loophole.** Workers on probation get the same hearing on shorter clocks: charges 5 days ahead, a panel of three drawn the same way (from other co-ops if need be) that must be unanimous, a decision within 15 days of the charges, and the same appeal. The co-op checks first for a cause it has to accommodate, and any plan to improve starts only once an accommodation is in place and runs at least 30 days. The membership clock keeps running: a worker whose case isn't settled when probation ends becomes a member, subject to the outcome. That makes a three-month probation hard to end early for performance. The model accepts that. The test against pushing people out only sees patterns, so one person's case needs its own protection. Dismissing them for performance still counts in that test. Only the narrow reasons listed there, like a client's safety, are excluded.
|
||||
- **The savings are safe.** A dismissed member's [capital account](capital-accounts.md) is fixed on the day they leave, they get a share of that year's profit for the hours they worked, and it's paid on the normal schedule. Nothing can be taken out of it without a court or arbitrator's ruling.
|
||||
|
||||
**Published.** Every year the co-op reports warnings, hearings and dismissals by gender, race, age and disability, along with the results of an anonymous survey on workload and bullying.
|
||||
|
||||
## Where the share is small anyway
|
||||
|
||||
In low-margin co-ops, like home care, a full profit share is worth a couple of thousand dollars a year, on top of the pay, schedules and say in the work that members get. There, the study's warning applies: a small shared stake may not motivate anyone. So those co-ops can't count on the profit share to do the work, or on bonuses either: half of a thin profit is a small bonus, and gain sharing from a team's own savings is limited to 3% of its pay. Pay, schedules, training and a real say in how the work is done matter more.
|
||||
|
||||
## Measuring it
|
||||
|
||||
The pilot publishes value added per hour worked (what a firm's work adds, after the cost of materials and services it buys, before wages and profit are split), compared with matched conventional firms ([The Pilot, in Numbers](pilot-spec.md#what-gets-measured)). It follows each co-op over time as it grows, in size bands set in advance, and accounts for how much equipment each firm uses per hour. Spin-offs are counted with their parent, and co-ops that close stay in the results. Injuries and overtime are published alongside, so output bought with worn-out workers doesn't count as success.
|
||||
|
||||
If co-ops fall behind as they grow, that's evidence the 1/N problem is real, and it gets reported that way.
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **Big co-ops are still harder.** People hold each other to account best in small places. Teams and spin-offs help. A co-op of 10,000 will still feel more distant than one of 30.
|
||||
- **Everyday pressure doesn't reach a hearing.** Co-workers who hold each other to account can also pick on people who are different. The protections, the published figures and the anonymous survey make that visible. They can't stop all of it.
|
||||
- **The process takes time.** A month from charges to decision is fast for a fair process and slow for a small business with a problem. That's the trade.
|
||||
- **Team bonuses can pit teams against each other.** Members choose what to reward, and some will choose badly.
|
||||
- **The numbers are first drafts.** The 10% and 15% bonus limits, the four-of-five panel and the 50-worker line for monthly books are judgments, not findings. A stricter panel protects members from a hostile majority and makes it harder to remove someone who really won't do the work. The pilot's published hearings and dismissals are how to tell whether the balance is right.
|
||||
- **The evidence is thin.** The best study asks workers what they'd do and what they've done, in conventional firms. The pilot is the first real test of these rules in co-ops.
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
title: When Growing Means Sharing
|
||||
section: The Co-op
|
||||
order: 29
|
||||
summary: Members who split profit equally have a reason to turn down growth that earns less per worker than they already do, so the model lets co-ops grow by starting new co-ops they invest in, under rules that stop that becoming a way to push lower-paid work out, and measures whether they grow as fast as other firms.
|
||||
---
|
||||
|
||||
# When Growing Means Sharing
|
||||
|
||||
A conventional owner takes on a new line of business if it earns more than the money it ties up would earn elsewhere. How many people it employs doesn't matter.
|
||||
|
||||
A co-op's members decide differently. After the [ramp](worker-ownership.md), they split profit equally. So a new line has to earn at least as much per worker as the co-op already does, or it lowers everyone's share. **A line that makes money can still make each member poorer, unless it's set up as a co-op of its own, which this model makes the standard path.** Economists have known about the problem since Benjamin Ward described it in 1958, and it's a real weakness.
|
||||
|
||||
## A worked example
|
||||
|
||||
A 40-member co-op has a members' profit pool, the part left after the reserve and the federation fund ([Member Capital Accounts](capital-accounts.md#where-each-years-profit-goes)), of $800,000 a year. That's $20,000 per member.
|
||||
|
||||
It has a chance to open a new line. The line needs 20 more workers, pays them normal salaries, and adds $200,000 a year to the pool: $10,000 per new worker. Suppose that's comfortably more than the money it ties up would earn anywhere else. A conventional owner would take it.
|
||||
|
||||
For the members, it depends on the ramp. New members start at a fifth of a full share and reach a full share after five years. The founding plan allows up to ten.
|
||||
|
||||
| Year | Each full share, 5-year ramp | Each full share, 10-year ramp |
|
||||
| --- | --- | --- |
|
||||
| No new line | $20,000 | $20,000 |
|
||||
| 1 | $22,700 | $23,800 |
|
||||
| 3 | $19,200 | $21,700 |
|
||||
| 5 | $16,700 | $20,000 |
|
||||
| 10 and after | $16,700 | $16,700 |
|
||||
| **Total over 10 years** | **$181,000** (vs. $200,000) | **$199,000** (vs. $200,000) |
|
||||
|
||||
(The 10-year ramp here starts at a tenth of a share and adds a tenth each year.)
|
||||
|
||||
**Tax law changes this table.** It assumes the part of a share new members haven't reached yet goes to the other members. Under the model's default tax rules, it goes to the co-op's reserve instead ([How a Locked Co-op Is Taxed](taxes.md#the-tenure-ramp)). Then each full share drops to about $16,700 from the first year, and in year 1 the reserve gets about $267,000 extra with the five-year ramp. The table holds only for a co-op that pays the difference as wages for years of service, at the cost of payroll tax. Either way, that makes the spin-off route below more important.
|
||||
|
||||
A member near retirement comes out ahead either way. A member with twenty years ahead loses about $50,000 over that time with a five-year ramp, and about $35,000 with a ten-year one. Counting money later as worth less than money now (at 7% a year), those losses are about $23,000 and $10,000. The gains come first and the losses later, so members who expect to leave soon and members who expect to stay long will vote differently. The ramp softens the problem. It doesn't remove it. In the end, **the new line has to earn the co-op's average, not just a profit, before the members come out even.**
|
||||
|
||||
**Where it bites, and where it doesn't.** The gap only matters when profit per member is high. In home care, a full share is worth a couple of thousand dollars a year ([When People Don't Stay Long](high-turnover.md#the-ramp-when-people-leave-early)), so a new line that earns less costs each member a few hundred dollars at most. In a firm making tens of thousands of dollars per worker, the kind described in [What If Conservice Had Been a Co-op?](conservice.md), it's real money. And the most profitable co-ops are the ones whose growth matters most.
|
||||
|
||||
## What the evidence says
|
||||
|
||||
**Co-ops are steadier, and slower to hire.** A study of Italian firms from 1982 to 1994, in the market economy with the most worker co-ops, found co-ops paid about 14% less on average. Their pay moved more with good and bad times, and their employment moved less, in both directions. When business picked up, co-ops added fewer workers than conventional firms. The co-ops in that data were larger on average than conventional firms, not smaller, though that comparison doesn't account for which industries they're in ([Pencavel, Pistaferri and Schivardi, 2006](https://docs.iza.org/dp2188.pdf)). A study of every co-op and conventional firm in Uruguay's social security records from 1996 to 2005 found the same pattern: co-op pay responds more to shocks, and employment less ([Burdín and Dean, 2009](https://doi.org/10.1016/j.jce.2009.08.001), as summarized by [Pencavel, 2012](https://docs.iza.org/dp6932.pdf)).
|
||||
|
||||
In France, a comparison across many industries found worker co-ops grew as fast as conventional firms or faster ([Fakhfakh, Pérotin and Gago, 2012](https://ideas.repec.org/a/sae/ilrrev/v65y2012i4p847-879.html)).
|
||||
|
||||
Ward's starkest prediction, that co-ops would *cut* jobs when prices rise, doesn't show up. What shows up is slower hiring and steadier jobs. The steadiness that protects members in bad years is the same thing that makes them slow to hire in good ones.
|
||||
|
||||
One caution: co-ops in Italy and Uruguay can hire non-members. Co-ops under this model mostly can't (below). So that evidence probably understates the problem here.
|
||||
|
||||
**When hiring members is costly, co-ops hire non-members instead.** The Olympia Veneer Company, the first of the Pacific Northwest plywood co-ops, started in 1921 with 125 worker-members. As founders left, the co-op bought back their shares and hired ordinary employees in their place. By 1952 fewer than 50 members were left, and in 1954 they sold the company for a handsome return. Mondragon's industrial co-ops saw non-members reach 29% of their workforce in 2000, before new rules brought it down to 12% by 2008. Counting the ordinary companies Mondragon owns in Spain and abroad, non-members went from 10% of its workforce in 1990 to 70% in 2007 ([Pencavel, 2012](https://docs.iza.org/dp6932.pdf), citing Arando and others, 2011).
|
||||
|
||||
That route is closed here. [The lock](the-lock.md) requires members to work at least three-quarters of the hours, and any company a co-op controls is locked too. That stops a co-op from turning into a small club of owners with employees under them. But it also means a co-op can't get around dilution by hiring outsiders. So dilution bites *harder* here than in co-ops that allow hired labor. The model accepts that, because the alternative is the road the plywood co-ops took out of being co-ops.
|
||||
|
||||
## The model's answers
|
||||
|
||||
### 1. Grow by starting new co-ops
|
||||
|
||||
Mondragon found a way through early. When its first factory, Ulgor, grew new lines of business, it turned them into co-ops of their own. Its electronics division became a new co-op, Fagor Electrónica, in 1965, "continuing with the plans for creating spin-offs from Ulgor" ([Mondragon, *The Mondragón Co-operative Experience, 1956–2012*](https://laboress-afrique.org/boostsess/public/uploads/pdfdocuments/files/mondragon_1956_2012.pdf)).
|
||||
|
||||
But spin-offs weren't the whole system. Ulgor and its spin-offs formed a group, Ularco, that began partly pooling its co-ops' profits in the late 1960s and pooled them completely from 1976. When Mondragon reorganized in 1992, it planned to pool between 15% and 40% of profits across each of its new sectors. Workers moved between co-ops when one shrank. From 1979 to 1983, when Ulgor had to cut 1,300 of its 3,500 jobs, 145 members split off to form a new co-op and others moved within the group, and every move was voluntary ([Morris, *The Mondragón System*, 1992](https://www.ilsr.org/wp-content/uploads/files/images/mondragon.pdf)).
|
||||
|
||||
The model makes spin-offs the standard path for a new line that earns less per worker than the parent:
|
||||
|
||||
- **The new line becomes its own locked co-op,** with its own members, its own profit split, and a [lock share](the-lock.md#how-the-lock-is-made-to-hold) held by an independent lock-holder from the day it's founded.
|
||||
- **The parent invests, but doesn't control.** It can fund the new co-op from its reserve, on the terms any investor gets ([Capital, After](capital.md)): repayment plus a share of profits, for at most 15 years, with no vote. If the new co-op borrows from the bank, the parent's claim ranks behind the bank and needs the bank's approval, like any other.
|
||||
- **Reserve money stays reserve money.** When the new co-op repays the money, it goes back into the parent's reserve. Only the profit share counts as profit, and the reserve takes its cut of that too ([Investing for the Long Run](the-long-run.md)). No single spin-off can take more than 15% of the parent's reserve. Up to a quarter is allowed only by a majority of all members in a secret ballot, the same bar [the lock](the-lock.md#when-a-co-op-owns-another-company) sets for buying a company. "Take" counts everything the parent has at stake: money put in and not yet repaid, guarantees of the spin-off's loans, assets sold to it on credit, and rent or bills left unpaid for more than 90 days. Spin-offs in the same business, and spin-offs of spin-offs, count as one. A stake written off in the last five years still counts, so a parent can't lose 15% and then start again. The test uses the reserve at the start of the year, when new money goes in. A reserve that shrinks later doesn't force a sale, but no new money goes in until it's back under the limit.
|
||||
|
||||
Federal law limits how much a national bank may lend one borrower without collateral to 15% of the bank's capital ([12 U.S.C. 84](https://www.law.cornell.edu/uscode/text/12/84)). This limit is stricter, because the reserve is only part of a co-op's own capital; members' accounts are the rest. It needs to be. A parent's stake has no collateral, it ranks behind the spin-off's lenders, and new businesses close a lot: only about 35% of American business locations opened in 2013 were still open ten years later ([Bureau of Labor Statistics](https://www.bls.gov/bdm/us_age_naics_00_table7.txt)). And a failed stake is a loss like any other, shared between the reserve and members' own accounts, with members paying most of it ([Investing for the Long Run](the-long-run.md#spending-it-badly)).
|
||||
|
||||
All of a parent's stakes in spin-offs together stay under half of its reserve, and under a third of its assets, not counting cash and government bonds. Above 40%, federal law can treat a company that holds investments as an investment fund, with its own heavy rules ([15 U.S.C. 80a-3(a)(1)(C)](https://www.law.cornell.edu/uscode/text/15/80a-3)). A co-op that mainly runs its own business is probably outside that law anyway, but the cap keeps it from having to argue the point.
|
||||
- **The spin-off is still a co-op.** Its own workers become members, and it's in the same federation as the parent, so a failure in one doesn't leave the other's workers with nothing ([The Federation](federation.md)).
|
||||
|
||||
In the example, the parent's members keep their $20,000 and earn a return on what they put in. The 20 new workers build full shares of their own co-op's profit instead of partial shares of someone else's. Without the spin-off, the members would most likely have turned the line down, and the jobs wouldn't exist.
|
||||
|
||||
This matters most for exactly the case the founding plan worried about: **expansion that takes a lot of capital, into lower-margin work.** The parent's reserve can pay for it without anyone's share shrinking.
|
||||
|
||||
**Yes, this makes richer and poorer co-ops.** The spin-off's members earn a smaller profit share than the parent's. That's the honest cost. The model's line is that inside one firm, where people share a vote and a set of books, shares are equal. Between firms, they aren't, as they aren't between any two companies. What stops that becoming a way to push lower-paid workers out is the next set of rules.
|
||||
|
||||
### 2. Rules so spin-offs can't be abused
|
||||
|
||||
- **New lines only, unless the workers choose to go.** A co-op can't spin off work its members already do unless a majority of the workers whose jobs would move vote for it. Anyone who doesn't want to move keeps their place in the parent.
|
||||
- **A spin-off that mostly works for the parent gets the parent's terms.** The worry is a parent moving its own work into a captive spin-off whose members get a smaller share. So if a spin-off makes more than 40% of its sales to the parent in any two of three years, counting sales through a middleman, through the parent's other spin-offs, or through firms its members own, the parent has to close the gap. The spin-off's workers choose, by secret ballot: membership in the parent with the tenure they've built, or a profit pool with the parent (below) that makes shares per member equal. The test is stricter than the one [the lock](the-lock.md#when-a-co-op-owns-another-company) uses for an outside supplier (most of its output, two years running), because the parent created this company. Prices between the two are still checked every year, as below.
|
||||
- **Deals between parent and spin-off are insider deals.** Prices, fees, rent and contracts between them must match what outsiders would pay, and they're checked every year, like any [insider deal](pilot-safeguards.md#the-hidden-boss). The lock's limit on exclusive contracts applies: no contract gives one party a claim on most of what the other produces for longer than five years ([The Lock](the-lock.md#closing-the-back-doors)).
|
||||
- **Assets move at an independent appraisal.** Equipment, customers or buildings moved to the spin-off count as a sale of part of the business, which goes through the lock-holder. The parent keeps any debts and obligations from before the split. So a spin-off can't be used to park a failing line or old liabilities.
|
||||
- **Those who move don't vote on the money.** Parent members who plan to join the spin-off can't vote on the parent's investment in it. They keep their tenure on the ramp only if the spin-off's founding papers agree to it, so senior members can't move over and dilute the new members in their turn.
|
||||
- **A second opinion on big investments.** Before a parent puts more than a tenth of its reserve into one spin-off, counting everything it put in over the last three years, the federation gives a written opinion on the plan to the parent's members. It's advice, not a veto. The investment is the members' call. Bigger commitments also need the independent adviser's opinion that [every large commitment](the-long-run.md#spending-it-badly) needs.
|
||||
|
||||
### 3. Sharing across the group, if the co-ops choose
|
||||
|
||||
Parent and spin-off can agree to pool part of their profits and split it per member across both, as Mondragon's groups did. That narrows the gap between them. It also brings back part of the dilution the spin-off avoided, which is why it's up to them. A pool of 15% to 40%, the range Mondragon settled on, is a reasonable place to start. The federation offers a standard agreement.
|
||||
|
||||
### 4. Where a line can't be split off
|
||||
|
||||
Some growth can't be spun off. More customers for the same service, or a cheaper product sold by the same team, can't easily become a separate company. There, the dilution stays. What softens it:
|
||||
|
||||
- **The ramp.** A co-op can choose the long end, ten years. That comes at a cost to newer members, who are more often younger or outsiders, and to workers who don't stay long ([When People Don't Stay Long](high-turnover.md#the-ramp-when-people-leave-early)).
|
||||
- **The founding share is a fixed slice.** Founders' extra reward is a set share of the pool, not extra weight per head, so founders gain nothing by keeping the co-op small once it's past its first three years ([The Founder Problem](founder-problem.md)).
|
||||
- **Growth that earns the average is free.** Adding workers to work that's as profitable as the rest doesn't dilute anyone. The problem is only lower-margin growth.
|
||||
- **Someone else can take it.** A line the members turn down can be offered to the federation, or to a group of workers, to start as a new co-op.
|
||||
|
||||
### 5. Measuring it
|
||||
|
||||
The pilot already publishes how many jobs its co-ops create ([The Pilot, in Numbers](pilot-spec.md#what-gets-measured)). It also publishes, with the comparisons filed in advance:
|
||||
|
||||
- **Job growth compared with matched conventional firms,** in full-time-equivalent jobs, counted both with and without spin-offs, and broken out by thirds of value added per worker (a way to compare how productive firms are that doesn't depend on how a co-op splits pay and profit). The gap should show up most in the most productive co-ops. Growth is measured from each co-op's second full year to its tenth, the same way for its matched firms, and a firm that closes counts as losing all its jobs, so co-ops that survive by not growing aren't flattered or punished by who's left. Value added isn't reported for the matched conventional firms, so the thirds come from each co-op's own value added per worker in its second full year, before growth is measured, and each co-op is compared only with its own matched firms. Productivity is also tested as a sliding scale across all co-ops, not just in thirds. Young firms' job growth varies enormously. Among American firms under five years old that stayed open, the fastest-growing tenth grew about 110 percentage points a year faster than the slowest tenth, and a few very fast growers pull the average up ([Decker and others, 2016](https://www.nber.org/papers/w21776)). On rough figures, about 130 co-ops in the top third against about five matched firms each, the smallest gap the comparison could reliably find is something like 12 to 20 percentage points a year, depending on how many close and how the reviewers handle the outliers. So **the prediction that the gap shows up most in the most productive co-ops probably can't be tested at this scale.** The reviewers file the time span, the handling of outliers and the power calculation before the first loan. A finding of no gap doesn't mean co-ops grow as fast.
|
||||
- **Expansions voted down.** Each co-op reports every major expansion proposal that reached its board or a members' vote, and what happened to it. "Major" means a new line of business, a new location, a spin-off, or anything expected to add at least a tenth to the co-op's hours or to spend at least a tenth of its reserve. Related proposals within a year are added together, so splitting one plan into small pieces doesn't hide it. Any member can put a proposal on the list. Lines offered to the federation or to other workers after the members turned them down are counted too. The yearly audit checks the list against actual hiring and spending. A proposal nobody writes down still can't be counted, so this undercounts.
|
||||
- **Spin-offs:** how many, how they do, and the gap in pay and profit share per member between each parent and its spin-offs.
|
||||
- **Who gets hired.** Applicants and hires by gender and race, compared with the matched firms, since members who share profit have a reason to hire few people, and people like themselves.
|
||||
- **How hires heard about the job.** The share of hires who came through a member's referral rather than a public posting, and how long referred and other hires stay. Referred hires often stay longer, so referrals aren't bad in themselves. But hiring through friends and family is also the quietest way for a co-op to keep looking like its founders, and applicant counts can't see it.
|
||||
- **Who leaves during probation.** People let go or pushed out before they could join, by gender and race, pooled across co-ops. The push-out check ([No Pushing People Out](high-turnover.md#no-pushing-people-out-before-theyd-join)) looks at one co-op at a time and can't see a pattern that small.
|
||||
- **Compared with the local workforce.** Hires are compared with people doing the same work in the same county, from Census survey data, as well as with the matched firms.
|
||||
|
||||
None of this sets targets. It's measured because the incentive is predictable, not because co-ops are assumed to discriminate.
|
||||
|
||||
**Testing hiring directly.** Counting applicants can't show whether equally qualified people are treated alike. The standard check sends fake job applications that are identical except for a name suggesting the applicant's race or sex, and records who gets called back. The largest such study sent over 83,000 applications to entry-level jobs at 108 of the biggest American employers. About 24% got a call back within 30 days. Applicants with distinctively Black names got about 2.1 percentage points fewer calls, and the gap varied a lot between companies. Telling one company apart from the rest took up to 1,000 applications per company ([Kline, Rose and Walters](https://www.nber.org/papers/w29053)). So:
|
||||
|
||||
- **Co-ops are tested as a group, never one by one.** A small co-op fills a few jobs a year, far too few to judge on its own. The pilot's outside reviewers send applications to entry-level openings at funded co-ops at random times between years three and nine. The dates aren't announced. Each opening gets one pair of applications, one with a name suggesting a Black applicant and one a white applicant, because more would stand out at a six-person co-op. Jobs that need a license are skipped unless both applications can honestly show the same one.
|
||||
- **One main measure, filed in advance.** The difference in calls back within 30 days between the Black and white names, pooled over all the testing. It's reported on its own terms, against zero, and next to the published results for big employers. Sex, Hispanic names and other comparisons are reported too, corrected for testing many things at once.
|
||||
- **What it can find.** Before filing, the reviewers count how many entry-level openings co-ops actually post, from the pilot's own payroll records, and state the smallest gap the test could detect, allowing for the fact that openings at the same co-op aren't independent. As a rough guide, if co-ops posted about 1,500 openings, the smallest gap it could reliably detect would be somewhere around 5 to 6 points, more than twice the big-employer average. If openings fall short, the result is "not settled." Either way, **a test that finds nothing doesn't show co-ops hire fairly.** It can only catch co-ops being much worse than ordinary employers.
|
||||
- **No unwilling firms.** Co-ops agree to the testing when they borrow. Conventional employers never agreed to anything, so they aren't sent fake applications, even though that would make a cleaner comparison. The co-ops' result stands next to other studies' published results instead. Before any testing, the ethics board that approves [the support lottery](pilot-spec.md#the-support-lottery) signs off, and state laws on testers are checked. Fake applications are withdrawn as soon as a co-op responds. Afterward, a sample of co-ops is asked whether they spotted the test, and the rate is published.
|
||||
- **Every opening is posted, with exceptions.** As a loan condition, co-ops post every opening publicly for at least a week, with written criteria for the job, before filling it. Payroll start dates are checked against posting dates, so a posting that closes after the new hire started doesn't count. Exempt, but counted separately: recalling laid-off members, rehiring someone within three years, union hiring halls, and aides a home care client chooses.
|
||||
- **Discrimination is a loan breach, whatever the co-op's size.** Federal job-discrimination law covers only employers with 15 or more workers ([42 U.S.C. § 2000e(b)](https://www.law.cornell.edu/uscode/text/42/2000e)), and most co-ops are smaller. Every borrower agrees not to discriminate in hiring, pay or membership. Complaints go to the civil-rights agencies where they can act, and to the bank as a possible breach where they can't.
|
||||
- **What a bad result does.** Nothing happens to any single co-op on the test alone, because the test can't single one out. If co-ops as a group show a real gap, scored interviews and outreach beyond members' own networks become a condition of every new loan, and the test is repeated.
|
||||
|
||||
Spin-offs can borrow from the bank. Because they start with an established co-op behind them, they're reported separately and don't count in the pilot's main test, the same way the pilot handles businesses that aren't really new ([Guarding the Pilot](pilot-safeguards.md#relabeling-an-old-business-as-new)). That keeps them from flattering the survival numbers, and keeps a struggling low-margin spin-off from counting against the test.
|
||||
|
||||
If the most productive co-ops grow much more slowly than similar conventional firms, that's evidence the problem is real where it matters most, and it gets reported that way.
|
||||
|
||||
## What wasn't done
|
||||
|
||||
- **Profit shares by division.** Paying each member partly from their own division's profit would remove most of the problem inside one firm. But the founding plan says that after the ramp, everyone's share is identical. Spin-offs do create differences, but between separate firms with separate votes, not between people who govern the same firm together.
|
||||
- **Profit shares in proportion to pay.** Splitting profit by pay instead of equally would make lower-paid new hires cost the others less. That also breaks the equal share.
|
||||
- **More room for non-members.** It would make growth easier, and it's the road the plywood co-ops took out of being co-ops.
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **Not every line can be spun off.** Where growth has to happen inside one firm, members still have a reason to turn down work that earns less than their average. Reporting the expansions they vote down makes that visible. It doesn't stop it.
|
||||
- **Spin-offs cost something.** Separate books, a separate board and separate legal costs. For a small line, that may be more than it's worth. And splitting a line off a firm that shares its customers and managers can make both work less well.
|
||||
- **Reserve money can be wasted.** Money that can't be paid out is tempting to spend. Fagor, Mondragon's appliance maker, overexpanded and went bankrupt in 2013 ([Investing for the Long Run](the-long-run.md#whats-still-open)). The caps on spin-off stakes and the federation's opinion are limits, not a test of whether an investment is wise.
|
||||
- **The parent profits from people it doesn't work with.** For up to 15 years, the parent collects a share of the new co-op's profit. It's finite, it carries no vote, and dealings between them are checked at outside prices. That's the model's line between renting capital and owning people's work ([Capital, After](capital.md)). Some will say it's still the wrong side of that line.
|
||||
- **Richer and poorer co-ops.** Spin-offs will often be lower-paid than their parents, and lower-margin work is disproportionately done by women and workers of color. Voluntary pooling and published gaps are the answer offered. Neither closes the gap.
|
||||
- **Steady employment cuts both ways.** Co-ops keep people on in bad years and hire slowly in good ones. Where that means fewer jobs than the business could support, the people who don't get hired pay for it, and they don't get a vote.
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: When People Don't Stay Long
|
||||
section: The Co-op
|
||||
order: 28
|
||||
summary: In jobs like home care, most workers leave within a year or two, so the model makes membership automatic after a short probation, keeps the buy-in small and out of people's paychecks, and guards against co-ops that push people out before they'd join.
|
||||
---
|
||||
|
||||
# When People Don't Stay Long
|
||||
|
||||
This model's co-ops are run by their members, and it takes membership seriously. After a trial period (probation) of at most 12 months, workers become members, and members must do at least three-quarters of the work hours ([The Lock](the-lock.md)). That rule stops a co-op from turning into a small club of owners employing everyone else.
|
||||
|
||||
But in some of the jobs where co-ops matter most, people don't stay long. In those jobs, the rule as first written could fail a co-op that was doing everything right.
|
||||
|
||||
## The problem
|
||||
|
||||
**Home care loses most of its workers every year.** Home care aides help older and disabled people at home: bathing, meals, medication. An industry benchmarking survey published in 2024 put home care worker turnover at about 79% a year ([Home Health Care News, 2024](https://homehealthcarenews.com/2024/07/home-cares-industry-wide-turnover-rate-reaches-nearly-80/)). Turnover here means the number of people who left in a year, divided by the average number of workers.
|
||||
|
||||
**Co-ops do better, but still lose a lot of people.** In 2017 there were 14 home care co-ops in nine states, employing about 2,400 caregivers. One account puts their turnover at about 38%, against 82% for the industry at its peak ([OnLabor, 2022](https://onlabor.org/worker-retention-in-homecare-could-cooperatives-be-the-solution/)). That's a small, uncontrolled comparison, but it points one way.
|
||||
|
||||
**With that much turnover, many workers never get past a 12-month probation.** If people leave at a steady rate through the year, here's roughly what share of the workforce at any moment has been there long enough to join:
|
||||
|
||||
| Turnover per year | Past 3 months | Past 6 months | Past 12 months |
|
||||
| --- | --- | --- | --- |
|
||||
| 20% | 95% | 90% | 82% |
|
||||
| 38% (home care co-ops) | 91% | 83% | 68% |
|
||||
| 65% | 85% | 72% | 52% |
|
||||
| 80% (home care industry) | 82% | 67% | 45% |
|
||||
|
||||
This is a simple model, and it counts people, while the rule counts hours. Both of those make the real picture somewhat better: many people leave in their first few weeks, and people who stay tend to work more hours. But the point holds. **With a 12-month probation, a home care co-op can fail the three-quarters rule even if every eligible worker joins.**
|
||||
|
||||
**The biggest real example shows the other half of the problem.** Cooperative Home Care Associates in the Bronx was started in 1985 by 12 home health aides and is one of the largest worker co-ops in the country. It's also unionized, with 1199SEIU ([CHCA](https://www.chcany.org/about)). It offers ownership after just three months. But workers have to choose to buy a $1,000 share, paid over time from their paychecks. In 2005, about 70% of its aides were owners, and 95% to 97% of those owners worked full time, so by hours it may well have met this model's rule. At a smaller sister co-op in Philadelphia, about 40% of aides were owners ([PHI, 2005](https://www.phinational.org/wp-content/uploads/legacy/clearinghouse/homecarecooperatives_workerownership.pdf)). By 2021, one account put CHCA at over 1,000 owners among 2,600 employees ([OnLabor](https://onlabor.org/worker-retention-in-homecare-could-cooperatives-be-the-solution/)). That count may include office staff, and it's by head, not by hours. But it suggests that when membership has to be chosen and paid for, many workers don't take it up.
|
||||
|
||||
To be fair to CHCA: it has stayed a real, worker-governed co-op for about 40 years with many workers who weren't owners. A union contract covering everyone helps explain why. This model still sets a membership rule, because not every co-op will have CHCA's history or its union.
|
||||
|
||||
## Two bad answers
|
||||
|
||||
- **Loosen the three-quarters rule.** That lets a co-op run on non-members again, which is exactly the slide the rule exists to stop. In a high-turnover business, "most of the workers aren't members" can easily become permanent.
|
||||
- **Exempt care work.** That would exempt the co-ops whose workers are least able to protect themselves.
|
||||
|
||||
## The answer: let people in sooner, by default, at no cost up front
|
||||
|
||||
### Membership is automatic
|
||||
|
||||
When probation ends, a worker becomes a member **unless they say no**. They don't have to apply, and nobody has to approve them.
|
||||
|
||||
This matters more than it sounds. When one large company started signing new workers up for its retirement plan automatically, 86% of new hires took part, compared with 37% of workers hired just before, at the same length of time on the job. The biggest gains were among the lowest-paid, whose participation went from 13% to 80% ([Madrian and Shea, 2000](https://www.nber.org/papers/w7682)). People mostly stay with whatever happens by default. So the default should be membership.
|
||||
|
||||
A retirement plan and a co-op aren't the same, and the same study found that people signed up by default mostly stayed passive about their choices. A member signed up by default may not act like an owner at first. The model's bet is that having the vote is the precondition for using it.
|
||||
|
||||
- **Saying no is easy and goes through someone independent.** A worker can opt out by phone, in person or in writing, in their own language, with the co-op's [lock-holder](the-lock.md#how-the-lock-is-made-to-hold), the independent body that guards each co-op's lock, rather than with their manager. So managers can't quietly talk people out of joining. Anyone who opts out can change their mind within 30 days, or join later at any time.
|
||||
- **Members and non-members get the same pay and schedules.** A co-op can't pay non-members more, or give them better shifts, to keep them from joining.
|
||||
- **People who freely opt out don't sink the co-op.** Hours worked by people who opted out through the lock-holder count toward the three-quarters rule, up to one-tenth of all hours. Beyond that, they count as non-member hours. So a co-op isn't punished for a few workers' free choices, but it can't run on them either.
|
||||
|
||||
### Joining costs nothing up front
|
||||
|
||||
Existing home care co-ops charge refundable fees of $25 to $1,000, often taken from paychecks a little at a time (PHI, 2005). That's a real cost for someone earning an aide's wage, and in many states a deduction from pay needs the worker's written permission.
|
||||
|
||||
So in this model, **the buy-in comes out of the member's own profit shares, not their paycheck.** It's capped at $1,000. Until it's covered, part of each profit share goes toward it instead of being paid out in cash, though never the cash a member needs to pay tax on their share. It's held apart from their [capital account](capital-accounts.md), at its face value. It earns no interest and isn't charged with the co-op's yearly losses, and it's paid back in full within a year after they leave. Nobody pays it from their paycheck, so nobody puts cash into the co-op to join ([Member Capital Accounts](capital-accounts.md#are-accounts-securities)).
|
||||
|
||||
### A short probation
|
||||
|
||||
Probation still can't be longer than 12 months. But a co-op can make it shorter, and the model's standard bylaws use three months, as Cooperative Home Care Associates does. The table shows why a high-turnover co-op will want that: at 38% turnover, a three-month probation puts about 91% of the workforce past it.
|
||||
|
||||
**Why letting people in early is affordable.** It's the [profit share ramp](worker-ownership.md). A new member starts with a small slice of the profit that grows over the years. So a new member takes only a small slice of the profit at first. The rest of what their hours earn goes to the co-op's reserve, not to the other members, because of how federal tax law treats profit shares ([How a Locked Co-op Is Taxed](taxes.md#the-tenure-ramp)). What the new member gets right away is the vote. Under this model, that doesn't wait. Workers on probation get a profit share too, in cash, at the rate of a member just starting out.
|
||||
|
||||
### No pushing people out before they'd join
|
||||
|
||||
If membership comes at three months, a co-op that wanted to stay a small club could let people go at two and a half, or cut their hours until they quit. So:
|
||||
|
||||
- **Every hour counts, whoever signs the paycheck.** Hours worked on the co-op's business by staffing-agency workers, contractors or a related company's staff count as non-member hours. A co-op can't get around the rule by putting its workers on someone else's payroll.
|
||||
- **Probation counts across rehiring.** Time on probation adds up across every stint at the same co-op within three years. Letting someone go and rehiring them doesn't restart the clock.
|
||||
- **Pushing people out counts as letting them go.** If someone on probation quits within 60 days after their hours were cut by a quarter or more, or after they lost their regular clients, it counts as the co-op's decision, not theirs.
|
||||
- **The test looks at results.** Each year the co-op reports how many people it hired, how many it let go (counted as above) before they could join, and why. The lock-holder compares that with the same co-op's record after people become members, and with similar co-ops. It looks only once there are enough cases to mean something, at least five over three years. Dismissals for a documented reason, like a client's safety or a failed background check, don't count. A worker can also complain directly to the lock-holder.
|
||||
- **A finding has to be earned.** Before a pattern counts against the co-op, the co-op sees the evidence and can answer it. A confirmed pattern counts as failing the membership rule for that year.
|
||||
|
||||
### A start-up period
|
||||
|
||||
A new co-op that's hiring fast will always have many people on probation. So the rule phases in. There's no hours test in the first year, members must do at least half the hours in the second, and three-quarters from the third on. Automatic membership and the 12-month limit apply from day one.
|
||||
|
||||
### Unions stay
|
||||
|
||||
Becoming a member doesn't end a union contract. A union can keep representing workers, members and non-members alike, as 1199SEIU does at CHCA. Federal labor law may not protect members' own organizing, though, so the charter does ([Owners Are Still Workers](labor-law.md#union-rights-the-biggest-gap)). Members are also paid for time spent on the co-op's committees and board, as CHCA and its sister co-op already do (PHI, 2005), so taking part in governance doesn't cost an aide a shift.
|
||||
|
||||
## The ramp, when people leave early
|
||||
|
||||
The founding plan sets the profit share ramp at five to ten years. In a job where many people stay two or three, most will leave before they ever reach a full share. At 38% turnover with a three-month probation and a five-year ramp, only about one worker in seven has a full share at any moment. That's a real cost of the ramp, and it's worth saying plainly.
|
||||
|
||||
Some things make it less harsh than it sounds:
|
||||
|
||||
- **The vote doesn't ramp.** A member who stays one year has the same vote as one who stays thirty.
|
||||
- **Time on the ramp adds up.** Someone who leaves and comes back within a few years picks up where they left off, and protected leave, like parental or medical leave, counts as time worked ([Worker Ownership](worker-ownership.md)). That matters in care work, where many workers, most of them women, step out for a while to care for their own families.
|
||||
- **Whatever they built goes with them.** A member's account keeps paying out on its normal schedule after they leave, and small balances are paid all at once ([Member Capital Accounts](capital-accounts.md#how-its-paid-out)). But where most payouts would go to people who've left, pension law makes accounts risky, so co-ops with turnover this high almost always pay the members' share all in cash instead. Everyone is still signed up for an ordinary retirement plan outside the business, with the co-op adding 3% of wages and a federal match for low earners from 2027 ([Member Capital Accounts](capital-accounts.md#saving-for-retirement)).
|
||||
- **In care work, the profit is small anyway.** At home care's thin margins, a full share is worth a couple of thousand dollars a year, and a typical member's is less ([Member Capital Accounts](capital-accounts.md)). For these workers, most of what matters is pay, hours and a say in how the work is done.
|
||||
|
||||
A co-op can choose the short end of the ramp. This page doesn't shorten it further, because the founding plan sets that range on purpose. A longer ramp was meant to let a co-op hire without every new member cutting into everyone else's share right away. Under tax law, it only does that if the co-op pays the difference as wages for years of service ([How a Locked Co-op Is Taxed](taxes.md#the-tenure-ramp)). Whether low-margin, high-turnover co-ops should be allowed a much shorter ramp is an open question ([Open Problems](open-problems.md)).
|
||||
|
||||
## When Medicaid decides how much goes to pay
|
||||
|
||||
A federal rule adopted in 2024 says that at least 80% of what Medicaid pays an agency for personal care, home health aide and homemaker services has to go to the direct care workers who give it ([42 CFR 441.302(k)](https://www.ecfr.gov/current/title-42/section-441.302)). It starts on July 9, 2030, or with the first managed care contract year after that where a state pays through private plans.
|
||||
|
||||
- **Who counts as a direct care worker.** Aides and personal care attendants, but also the nurses who give care and the staff who supervise it clinically.
|
||||
- **What counts as their pay.** Wages, benefits like health insurance, paid leave and retirement, and the employer's share of payroll taxes. Training, travel and protective equipment come off the top before the 80% is measured.
|
||||
- **What it covers.** Only these services paid under Medicaid's home and community-based waivers and three similar options. It doesn't reach Medicare, private-pay clients, or Medicaid's ordinary home health benefit, so how much it touches a co-op depends on who pays for its work. Care where the person receiving it sets the worker's rate isn't counted either.
|
||||
- **Exceptions.** A state can set a lower level for small agencies that meet criteria it sets, and exempt some agencies facing hardship, after public notice.
|
||||
|
||||
**It may not survive.** As of September 2026 the rule is in the federal regulations unchanged. But the home care industry has fought it since it was proposed, warning it would close agencies ([Home Health Care News, 2024](https://homehealthcarenews.com/2024/04/home-cares-industry-slams-finalized-80-20-rule-warns-agency-closures-are-coming/)), and it could be changed or dropped before 2030. The model neither needs it nor depends on its failing.
|
||||
|
||||
**What it does and doesn't do.** It sets a floor on the share of the rate that goes to care workers. It doesn't set a floor on pay: 80% of a low rate is still low pay, and an agency can meet it by spending more on health insurance with no raise. If rates don't rise, the likely costs are agencies leaving Medicaid, fewer hours for clients, and less money for scheduling and supervision. Rules like this can raise pay and can also cost access. Which one wins is an empirical question, and the model takes no side on it.
|
||||
|
||||
For a co-op, it means conventional agencies now have to do part of what a co-op promises. A co-op's case in Medicaid home care then rests less on pay and more on steadier hours, a say in the work, and keeping people longer, which is what [the pilot](pilot-spec.md) has to show.
|
||||
|
||||
### How a co-op meets it
|
||||
|
||||
- **Wages, benefits and payroll taxes alone reach 80%.** The profit share and account credits don't count. The model's position is that the share is an owner's share, not pay ([Owners Are Still Workers](labor-law.md#is-the-profit-share-pay)), and a co-op can't call it pay to Medicaid and not pay to the tax office. Leaving it out also stops a co-op from holding hourly pay down and making it up with a year-end share that may not come.
|
||||
- **Any shortfall is paid out as wages.** The shared payroll service works out the ratio every quarter and shows it to members. If the year ends below 80%, for any reason, the difference is paid to the direct care workers who worked that year, as an hourly wage supplement, before any profit is shared. That's plain wages, so it counts without anyone's ruling. It runs in the same payroll pass that already recalculates overtime ([Owners Are Still Workers](labor-law.md#overtime-and-the-profit-share)).
|
||||
- **Job titles are checked.** Which roles count as direct care follows the rule's list and state guidance, and the co-op's auditor checks it each year, so an office job can't be relabeled as supervision.
|
||||
- **The rest has to fit in the 20%.** Office staff, insurance, rent, the bank's loan payments, the reserve and any profit share all come out of what's left. In Medicaid home care that means profit shares and account balances near zero, and a reserve that grows slowly. So the bank lends to a co-op doing this work only if loan payments fit inside the 20% with no profit at all, and sets its grace periods to match.
|
||||
- **Shared services are priced at cost and published.** Fees the co-op pays the federation or the bank are listed in its yearly report, at cost, so the federation can't become a way to move money out the way related-party fees do at conventional agencies.
|
||||
- **No hardship exemptions.** A co-op backed by a public bank doesn't take a hardship slot from a struggling independent agency. It can use a state's small-agency level only if it meets the state's criteria like anyone else, and only with a written plan to reach 80% within three years.
|
||||
- **Where the rates are set.** The same 2024 rules require each state to have an advisory group on these rates that includes direct care workers, and to publish its advice ([42 CFR 447.203(b)(6)](https://www.ecfr.gov/current/title-42/section-447.203)). Rates are the real limit on pay, so the federation takes part in those groups, and in the public comment where states set small-agency levels and exemptions, alongside unions.
|
||||
|
||||
### Keeping the pilot fair
|
||||
|
||||
Co-ops are compared with conventional firms in the same industry, region and start year ([The Pilot](pilot-spec.md#who-co-ops-are-compared-against)), but that doesn't make them equally exposed to this rule. So for co-ops and comparison firms alike, the pilot records the share of revenue from covered Medicaid services, whether the firm used a small-agency level or an exemption, its reported ratio where the state publishes it, fees paid to related companies, and whether the state bargains with a care workers' union.
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **Ownership doesn't fix the pay.** Home care wages are low largely because much of the work is paid for by public programs like Medicaid, at rates the agencies don't set. A co-op can't pay more than it's paid. It can offer steadier hours, training and a say, and co-ops seem to keep people longer. But the pay problem needs better public rates, rules that send more of those rates to workers, like [the 80% rule](#when-medicaid-decides-how-much-goes-to-pay), which can raise pay but may also cost access, and unions bargaining for them, including with states and through wage boards like [Minnesota's](essential-work.md), along with [the floor](the-floor.md).
|
||||
- **The numbers are rough.** Turnover varies a lot between agencies, the home care industry figure comes from a survey of agencies that chose to take part, and the co-ops' 38% comes from a small count. The pilot will measure this directly and report [results by industry](pilot-spec.md#what-gets-measured).
|
||||
- **Automatic membership hasn't been tried at a large care co-op.** It should raise membership a lot. How much, and whether default members take part in governance, is unknown.
|
||||
- **A short probation means less time to judge fit.** A co-op admits people it has known for three months, and they vote. That's the trade-off the model accepts. The ramp limits what a poor fit costs in money. It doesn't limit their vote.
|
||||
- **The stable minority can still run things.** Office staff and coordinators often stay while aides come and go. One member, one vote and paid meeting time help. They don't guarantee aides a real voice.
|
||||
- **Some legal questions have answers now, and some don't.**
|
||||
- **Taxpayer numbers.** An individual taxpayer number from the IRS doesn't "authorize you to work legally in the U.S." ([IRS](https://www.irs.gov/tin/itin/individual-taxpayer-identification-number-itin)). Members have to be employees verified as allowed to work, so workers who have only that number can't be members. That's a gap in the model, not a fix ([How a Locked Co-op Is Taxed](taxes.md#what-this-doesnt-solve)).
|
||||
- **Work permits that lapse.** Many care workers work on permits that have to be renewed or can be ended, such as Temporary Protected Status. A member whose permit lapses has to come off the payroll. They keep their account, which pays out on its normal schedule, and for 12 months they keep their place on the ramp and rejoin without a new probation once they can work again. Workers on a visa tied to their employer get a free consult with an immigration lawyer through the federation before they're signed up. Owning part of a co-op doesn't raise the special rules some visas have for owners, which start at "more than 50 percent" of the employer or "majority voting rights" ([8 CFR 214.2(h)](https://www.law.cornell.edu/cfr/text/8/214.2)).
|
||||
- **Benefits.** The share and account credits that haven't come due shouldn't count as savings for SSI or food assistance, because the bylaws bar members from getting at them early. But SSI may count a credit as income when it's made, and may count the share as unearned income, which it treats harshly. So workers on SSI or disability benefits, found through a confidential check the federation runs, not the co-op, become members only if they say yes, and get their share all in cash until Social Security says how it counts it ([How a Locked Co-op Is Taxed](taxes.md#benefits)). Opting out doesn't change how a cash share is counted, and the notice says so.
|
||||
- **Unions.** Federal labor law probably doesn't protect union activity by members who own the firm. The charter protects it instead, and fixing the law takes a separate bill ([Owners Are Still Workers](labor-law.md#union-rights-the-biggest-gap)).
|
||||
- **Opting out can still be pressured, and pushing people out can still be hidden.** An independent lock-holder and results-based checks make both harder. They can't make them impossible.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Free-Market Democratic Socialism
|
||||
summary: A working model for building an economy where the people who do the work own and govern the companies they work for. Nobody's company gets taken, and markets, wages, investment and personal property all stay.
|
||||
---
|
||||
|
||||
# Free-Market Democratic Socialism
|
||||
|
||||
## The idea in one sentence
|
||||
|
||||
**Your house, car, savings and tools are yours, and nobody touches them. But a permanent, tradeable claim on the work of thousands of strangers is a different kind of thing, so there should be companies where that claim never forms, and nothing in law or finance should stop people from building them.** [Read the thesis](thesis.md).
|
||||
|
||||
## Nobody is forced
|
||||
|
||||
This model doesn't buy, seize or convert anyone's company. Instead it builds new worker-owned companies alongside existing ones, then lets customers and workers choose between them.
|
||||
|
||||
- **Workers own their company.** Every member gets one vote. A share of the profits grows with tenure until long-time members split them equally, per hour worked, which is how federal tax law lets co-ops pay tax on it only once. Salaries still exist, and hard jobs still pay more. Part of each year's profit goes into a personal account that pays it back a few years later on a fixed schedule, whether or not you've left, which is designed to keep it outside federal pension law, though courts haven't settled that; where most payouts would go to people who've left, as in high-turnover work, the share is paid in cash instead. Part goes into a permanent reserve that keeps the business investing. Members can't drain it through high pay or let the equipment quietly wear out, and big bets get an outside opinion first. An insurance pool the co-ops pay for protects workers' last paychecks and part of their savings if the co-op fails. A fair, fast process deals with members who won't do the work, with protections so it can't be turned on the wrong people. When business falls off, everyone shares the cut before anyone is laid off, and layoffs follow an order set in advance that treats members and non-members alike. Members are the co-op's employees, so they get the minimum wage, overtime and injury insurance like anyone else. That also means members who run payroll, and in New York possibly every member, can personally owe wages the co-op fails to pay, so a co-op that misses a payroll stops ordinary work until it's paid up. Where the law leaves owners out, of unemployment insurance in some states or of union protection, the co-op's charter fills what it can. Membership comes automatically after a short probation, so co-ops in high-turnover work like home care don't end up run by the few who stay. Where Medicaid requires 80% of home care payments to go to care workers, co-ops meet it from wages alone. Equal shares give members a reason to turn down growth into lower-paying work, so co-ops can grow by starting new co-ops they invest in instead. The checks behind all this are scaled to size, so small co-ops get a yearly review and random audits instead of a full audit every year, though honest checking still costs them real money. [Worker Ownership](worker-ownership.md), [Does Sharing the Profit Mean Less Effort?](free-riding.md), [When Work Runs Short](downturns.md), [When People Don't Stay Long](high-turnover.md), [Owners Are Still Workers](labor-law.md), [When Growing Means Sharing](growth.md), [Member Capital Accounts](capital-accounts.md), [How a Locked Co-op Is Taxed](taxes.md), [Protecting Members' Savings](savings-guarantee.md), [Investing for the Long Run](the-long-run.md) and [What the Rules Cost a Small Co-op](compliance.md)
|
||||
- **A small public bank lends to worker co-ops.** It charges market rates, so it only succeeds if co-ops can really compete. Its whole job is to be willing to lend to a kind of company other banks won't. Spain, Italy and the US have all tried versions of this, and the pilot is built around what went right and wrong. [The Bank](the-bank.md) and [Has This Been Tried?](precedents.md)
|
||||
- **Owners who want to sell to their workers can.** The bank lends for these sales on a separate track, with its own $100 million and its own test. Only healthy firms qualify, there's no new tax break, the price is capped at an independent appraisal, the seller shares the risk, and the whole workforce votes. It serves deals the SBA's rules shut out, which ask a co-op, unlike an ESOP, for cash up front and one worker's personal guarantee. Its results are compared with SBA-financed purchases and ESOP sales, and its pay test follows everyone who worked there at the sale. [When an Owner Wants to Sell to the Workers](buyouts.md)
|
||||
- **Co-ops funded this way stay co-ops forever.** They can't be sold to conventional owners, and members can't vote to cash out what earlier members built. An independent lock-holder can veto any attempt, including selling the business off piece by piece. If a co-op goes bankrupt, only another co-op can buy it whole, and its workers get a real chance to be that buyer. That needs an amendment to federal bankruptcy law, drafted here, and a short model state law closes the ways out that state law leaves open. [The Lock](the-lock.md), [When a Locked Co-op Goes Bankrupt](bankruptcy.md), [The Lock in State Law](lock-state-law.md)
|
||||
- **It runs as an honest experiment.** A one-time $350 million pilot aims to fund about 400 new co-ops. The success measures are published before the first loan. If, after twelve years, co-ops stay open about as well as similar conventional firms while paying their members fairly, and the bank earned at least what Treasury bonds would have, Congress votes on growing it. If not, it shuts down automatically. It's built so that a hostile government can't quietly freeze it or rig the result, though Congress can always repeal it. It also tests, by lottery, whether serious help before launch gets more co-ops started. Part of the loan money is set aside for co-ops in lower-paid work, so the bank can't drift toward only the safest co-ops, and the verdict has to say which kinds of co-ops it covers. The pilot can't show whether co-ops lift pay at other employers. Even after a pass, a sector big enough for that is decades away nationally. So growth comes in capped stages, and that effect gets its own test in a few local markets. For raising pay across a whole kind of work, wage boards and unions are faster, and the model doesn't replace them. [The Test](the-test.md), [The Pilot, in Numbers](pilot-spec.md), [Help Before the First Loan](before-launch.md), [Who the Bank Lends To](who-gets-loans.md), [After a Pass: How Big, How Fast](scale.md) and [Making It Last](durability.md)
|
||||
- **Co-ops back each other up.** They pool part of their profits into a shared fund. If one fails, its workers are moved into others instead of being left with nothing. [The Federation](federation.md)
|
||||
- **Nobody is left behind by low-margin work.** Owning a company doesn't help much when there's barely any profit to own, so a strong floor covers the rest. A fixed formula, not lobbyists, decides which low-paid work gets public help. [Ownership and the Floor](the-floor.md) and [Who Decides What's Essential](essential-work.md)
|
||||
|
||||
## What stays the same
|
||||
|
||||
| Stays | Why |
|
||||
| --- | --- |
|
||||
| Existing companies | Nobody's company is bought or taken. Conventional firms keep conventional ownership. |
|
||||
| Markets and competition | Co-ops and conventional firms compete for the same customers and workers. |
|
||||
| Salaries | Hard and in-demand jobs still pay more. A surgeon still earns a surgeon's salary. Pay bands are open, and high pay needs the lower-paid half to agree. [Who Sets the Pay](pay.md) |
|
||||
| Personal property | Homes, cars, savings and tools are untouchable. |
|
||||
| Investment and big returns | Investors can still fund co-ops and make 10x or 30x. Their claim just runs out and carries no vote. [Capital, After](capital.md) |
|
||||
|
||||
## Why it matters
|
||||
|
||||
This isn't mainly about money. **People deserve a say in governing the place where they spend most of their waking lives.** Today most workers are ruled by distant owners who answer to nobody who works there. Co-ops are rare, and one likely reason is that nobody will lend to them. This model fixes the financing and lets people choose. [Making the Case](the-argument.md) and, for one real company worked through, [What If Conservice Had Been a Co-op?](conservice.md)
|
||||
|
||||
## Honest about what's unsolved
|
||||
|
||||
This is a working model, not a finished one. The biggest open question is whether strong founders will start companies they can never sell. In the success case, a co-op founder ends up with a fraction of what a conventional owner would, because the rest goes to the people who work there ([The Founder Problem](founder-problem.md)). The pilot measures founders on both sides, so if it fails, the verdict has to say how much of that was down to who applied ([Co-ops or Their Founders?](who-founds.md)). The full list of what isn't solved yet is kept in the open. [Open Problems](open-problems.md)
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Intellectual Property
|
||||
section: Beyond Co-ops
|
||||
order: 100
|
||||
summary: Targeted patent reform would help new firms, but it's kept separate from the bank so the achievable part doesn't wait on the hard part.
|
||||
---
|
||||
|
||||
# Intellectual Property
|
||||
|
||||
Patent terms are too long for how fast many industries move, and some patents should never have been granted. Loosening IP would lower the barrier for any new firm, co-ops included. It belongs in the model, but it is **decoupled from the bank** for two reasons:
|
||||
|
||||
- **It's sector-dependent.** In software, patents mostly protect incumbents and trolls against entrants; weakening them helps new firms. In pharma and hardware, a patent is often the *only* thing keeping a small entrant from being copied to death by a competitor with better manufacturing and distribution. Blanket loosening can hand the advantage to scale, which is the opposite of the goal.
|
||||
- **It's politically far heavier.** A small bank is achievable. Patent reform means fighting every industry that lives on patents. Coupling them means the achievable thing waits on the hard thing.
|
||||
|
||||
So the IP reform is targeted: shorter terms where innovation cycles are short, a much stricter bar for software and business-method patents, and aggressive action against patent trolls. Where patents genuinely protect small inventors, leave them alone.
|
||||
@@ -0,0 +1,155 @@
|
||||
---
|
||||
title: Owners Are Still Workers
|
||||
section: The Co-op
|
||||
order: 24
|
||||
summary: Labor laws were written for workers with a boss, so a co-op member can fall through the gaps, and the model closes the ones it can with the co-op's own rules and names the ones that need a change in the law.
|
||||
---
|
||||
|
||||
# Owners Are Still Workers
|
||||
|
||||
A co-op member is two things at once: an owner of the business and someone who works there for pay. America's labor laws were mostly written for people who are one or the other. Most of them ask some version of "does this person have a boss?" In a co-op, the answer is "yes, and they also help elect the boss."
|
||||
|
||||
The model puts members on the payroll as employees ([Who can borrow](pilot-spec.md#who-can-borrow)). That gets them most of the protections any other worker has, but not all, and not in every state. This page goes law by law.
|
||||
|
||||
## Wages: members are employees
|
||||
|
||||
**Federal wage law covers members.** In 1961 the Supreme Court looked at a Maine co-op of women who knitted at home and were paid by the piece. It held that they were the co-op's employees, entitled to the minimum wage, even though they owned it. "There is nothing inherently inconsistent between the coexistence of a proprietary and an employment relationship," the Court wrote. What mattered was the reality: the co-op's managers set the rates and could expel members for bad work ([*Goldberg v. Whitaker House Cooperative*, 366 U.S. 28](https://www.law.cornell.edu/supremecourt/text/366/28)). Three justices dissented, saying a genuine co-op doesn't employ its own members, but that's not the law.
|
||||
|
||||
So members are owed at least the minimum wage and overtime pay for every hour, like anyone else. **A profit share doesn't count toward the minimum wage.** It's paid once a year, after the books close ([How a Locked Co-op Is Taxed](taxes.md)), and a wage has to be paid when it's earned.
|
||||
|
||||
### Founders' unpaid work
|
||||
|
||||
This catches one part of the model. Founders can put in their stake partly as unpaid work, recorded as back pay the co-op owes ([Help Before the First Loan](before-launch.md#should-founders-put-in-money-of-their-own), [The Founder Problem](founder-problem.md#the-tools)). Once the co-op exists and founders are its employees, working below the minimum wage breaks the law, even by agreement. Workers can't sign away the minimum wage.
|
||||
|
||||
There is an exception in federal law for an owner of at least 20% of a business who actively manages it ([29 CFR 541.101](https://www.law.cornell.edu/cfr/text/29/541.101)). The model doesn't rely on it. A locked share can't be sold, so it may not count as a real ownership stake. The exception doesn't reach teams of six or more. And it's federal only, so state wage laws may still apply. So the rule is:
|
||||
|
||||
- **Before the co-op is formed, work can be unpaid.** Founders who are working on their own venture don't yet have an employer. They sign the bank's short standard founding agreement as equal partners, so it's clear nobody is working for anyone else. Their logged hours are turned into back pay when the co-op forms.
|
||||
- **Once the co-op is formed, everyone is paid at least the minimum wage for every hour,** founders included, at the state or local minimum where that's higher. Only the difference between the minimum wage and the local median pay for the job can be recorded as back pay and counted toward the stake. The first loan can pay those wages.
|
||||
- **Nobody works for free who isn't a founder.** Anyone who works on the venture after the application goes in is either a named founder or paid.
|
||||
- **The bank asks the Department of Labor for an opinion letter** on this setup before lending starts, and follows state law where it's stricter.
|
||||
|
||||
That makes the stake a bit harder to earn in work, since hours after the co-op forms count for less. It's still much easier than a cash buy-in.
|
||||
|
||||
### Overtime and the profit share
|
||||
|
||||
Overtime is paid at one and a half times a worker's "regular rate," and a bonus that workers are promised in advance usually has to be added into that rate ([29 CFR 778.211](https://www.law.cornell.edu/cfr/text/29/778.211)). Both the profit share and the [team bonuses](free-riding.md#team-bonuses-the-members-design) are promised in advance.
|
||||
|
||||
There's a way out for the profit share. A written profit-sharing plan that pays out of the whole business's profits, by a set formula such as hours worked, isn't added into the regular rate. But the plan has to be open at least to every worker covered by the wage law, not just members, though it can require a minimum length of service ([29 CFR 549.1](https://www.law.cornell.edu/cfr/text/29/549.1)). It has other conditions too:
|
||||
|
||||
- **The model's profit share probably doesn't meet it.** The part credited to members' accounts isn't paid "within a reasonable period" or put in a trust, as the rule requires. And a share "may not be diminished because of any other remuneration," but the model's pay rule takes part of above-market pay out of the profit share ([Investing for the Long Run](the-long-run.md#the-rules)).
|
||||
- **So the whole profit share is added into the overtime rate.** Every year, once the profit share is set, the standard payroll service recalculates overtime for every worker who had any and pays the difference. Overtime is a small share of hours in most co-ops, so the cost is small.
|
||||
- **Team bonuses are always added into the overtime rate.** The standard payroll service does the math.
|
||||
|
||||
### Meetings are work
|
||||
|
||||
Time at a required meeting counts as working time under federal law ([29 CFR 785.27](https://www.law.cornell.edu/cfr/text/29/785.27)). So members are paid for general meetings they're expected to attend and for time on the board and committees, as [When People Don't Stay Long](high-turnover.md#unions-stay) already requires. That also keeps governance from costing low-paid members a shift.
|
||||
|
||||
### Is the profit share pay?
|
||||
|
||||
Members get two kinds of money: wages, paid through payroll at no less than the going rate, and a share of the profit, split by hours worked once the year's books close. The model's position is that **a member's profit share is an owner's share of profit, not pay.** It depends on the whole co-op making money, it's declared only after the year ends, and wages always come first ([How a Locked Co-op Is Taxed](taxes.md#wages-first)). But each law asks its own question, and some of the model's own rules point the other way. The share is split by hours, it grows with years of service, and workers who chose not to join get one too, in cash ([How a Locked Co-op Is Taxed](taxes.md#one-rate-per-hour-for-everyone-who-works)). For them it's simply a bonus. So the answers differ by law:
|
||||
|
||||
- **Tax.** The model argues it's a patronage dividend, which is how co-op tax law pays owners, and that it doesn't carry payroll tax. That's unsettled ([How a Locked Co-op Is Taxed](taxes.md#payroll-tax-on-profit-shares)). For lower-income members' credits that aren't taxed until paid, the co-op also follows the tax rules for deferred pay, as a precaution, without conceding that they apply ([How a Locked Co-op Is Taxed](taxes.md#credits-taxed-later-the-deferred-pay-rules)).
|
||||
- **Overtime.** Federal overtime law counts almost everything an employer pays, so the co-op adds the share into the overtime rate, as above. Its definition is broader than the tax law's, so this doesn't settle the tax question either way.
|
||||
- **Pension law.** Members' accounts are designed to stay outside it. The model argues first that nothing is held back until people leave. In the alternative, if the share counts as pay, it argues that it's a bonus under the Labor Department's rule. That fallback concedes, for pension law only, the point the model resists elsewhere ([Member Capital Accounts](capital-accounts.md#why-not-pay-it-out-when-you-leave)).
|
||||
- **Medicaid's home care pay rule.** From 2030, agencies must spend 80% of certain Medicaid home care payments on care workers' pay. That rule borrows the overtime law's broad meaning of pay, so the share might count. The co-op doesn't count it anyway, to keep one position everywhere, and pays any shortfall as wages ([When People Don't Stay Long](high-turnover.md#when-medicaid-decides-how-much-goes-to-pay)).
|
||||
- **State laws on paying wages.** This is the hard one. Many states require wages to be paid on time and in full, forbid employers from taking any back, require final pay within days when someone leaves, and don't let workers sign those rights away. California's definition of wages covers "all amounts for labor performed by employees," however they're calculated ([Cal. Labor Code § 200](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=LAB§ionNum=200)). It's unlawful there for an employer "to collect or receive from an employee any part of wages theretofore paid" ([§ 221](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=LAB§ionNum=221)), and no private agreement can set those rules aside ([§ 219](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=LAB§ionNum=219)). If a credit in a member's account counted as wages there, holding it for years, charging losses to it and pausing it in a bad year could all be illegal, for members who stay as well as those who leave.
|
||||
|
||||
State co-op law supports the owner's-share reading, but it doesn't settle it. Massachusetts, California and Washington each have a law for worker co-ops that lets a member's share of profit be paid in "cash, credits, written notices of allocation, or capital stock," lets the bylaws "permit the periodic redemption of written notices of allocation," and forbids payouts that would make the directors personally liable ([Mass. Gen. Laws ch. 157A, §§ 8–9](https://malegislature.gov/Laws/GeneralLaws/PartI/TitleXXII/Chapter157A/Section9); California's is Corporations Code § 12317, [added in 2015](http://www.leginfo.ca.gov/pub/15-16/bill/asm/ab_0801-0850/ab_816_bill_20150812_chaptered.html); Washington's is [RCW 23.78.090](https://lawfilesext.leg.wa.gov/Law/RCW/RCW%20%2023%20%20TITLE/RCW%20%2023%20.%2078%20%20CHAPTER/RCW%20%2023%20.%2078%20.090.htm)). Those laws say how the money can be paid. None of them says it isn't wages, and a wage law's ban on signing rights away is the more specific rule. How far each state's wage law reaches pay that depends on the whole business's profit differs from state to state and isn't settled. So:
|
||||
|
||||
- **Where it's unclear, the co-op doesn't bet on it.** The bank publishes a checked entry for each state, signed by a lawyer licensed there and reviewed every year, saying whether its wage law could reach account credits. Before lending in a state, the bank asks the state's labor agency for its view. Where the answer is "could," co-ops in that state don't use members' accounts. The members' share is paid all in cash, and a co-op that wants more money to invest sends more of its profit to the permanent reserve instead. Members there don't build savings in the co-op until the state's law changes.
|
||||
- **The model state law fixes it at the source.** It amends the state's wage-payment law itself, not only its co-op law: a written share of profit that a business allocates to its owners, under a formula set in advance and only after every worker has been paid at least the going rate, isn't wages for that law. It applies to any business that does this, not only co-ops, so it isn't a favor to one kind of firm. It sits in the short law that already recognizes locked co-ops ([Making It Last](durability.md#federal-test-state-copies)).
|
||||
- **Every credit comes with a written notice.** Each year, each person gets a notice showing the amount credited and when it's scheduled to be paid, which is what those co-op laws describe. It also says plainly that the credit isn't a wage, that it can be reduced by losses or paused until it's paid, and where to file a wage claim if they think wages are owed.
|
||||
- **The going rate is checked.** The owner's-share argument only holds if wages really come first. So the co-op's outside accountant tests each year that every worker was paid at least the going rate, against the public benchmark in [the pay rule](the-long-run.md#the-rules), and any shortfall is paid as wages before any profit is shared. In small co-ops that's a fixed test of every worker's pay rather than a full audit ([What the Rules Cost a Small Co-op](compliance.md#one-package-done-once-a-year)).
|
||||
|
||||
**What members give up.** Calling the share an owner's share has costs. Unpaid wages get priority in a bankruptcy, up to $17,150 per worker ([11 U.S.C. § 507(a)(4)](https://www.law.cornell.edu/uscode/text/11/507)). Account credits don't, and they rank behind the co-op's lenders ([Member Capital Accounts](capital-accounts.md#protecting-the-co-op-and-the-people-who-stay)). Unemployment and injury benefits are figured from wages, so the share doesn't raise them. Federal limits on garnishment cover any "bonus" paid for work ([15 U.S.C. § 1672](https://www.law.cornell.edu/uscode/text/15/1672)), so a cash share probably gets that protection anyway. The [savings insurance pool](savings-guarantee.md) covers part of the bankruptcy gap for people who have left.
|
||||
|
||||
### When wages go unpaid, members can owe them
|
||||
|
||||
Some states make a company's owners or officers personally pay wages the company doesn't, and federal law does the same for people who run it. The laws were written for owners who pocket the profit and leave the workers unpaid. In a worker co-op, the owners and officers are workers too, so the bill can land on the co-op's own members, including its lowest-paid ones. The members owed wages and the members who owe them can be the same people.
|
||||
|
||||
- **Federal law reaches people in charge, in every state.** The Fair Labor Standards Act counts as an employer "any person acting directly or indirectly in the interest of an employer" ([29 U.S.C. § 203(d)](https://www.law.cornell.edu/uscode/text/29/203)). Courts have read that to reach officers and managers who control the business's operations, and an unpaid minimum wage or overtime is owed again as damages, so twice over ([§ 216(b)](https://www.law.cornell.edu/uscode/text/29/216)). It covers only the minimum wage and overtime, not the rest of a worker's pay. No state law can take this away.
|
||||
- **New York appears to reach every member.** For an ordinary private corporation or LLC, only the ten largest owners are personally liable for unpaid wages ([Business Corporation Law § 630](https://newyork.public.law/laws/n.y._business_corporation_law_section_630); [LLC Law § 609(c)](https://newyork.public.law/laws/n.y._limited_liability_company_law_section_609)). For a co-op it's broader: every member and every director of a cooperative corporation is personally liable, each for the whole amount, for wages owed to its employees ([Cooperative Corporations Law § 47](https://newyork.public.law/laws/n.y._cooperative_corporations_law_section_47)), and the co-op can't opt out of that in its charter. The state's worker co-op article applies the co-op chapter except for listed sections, and § 47 isn't listed ([§ 85](https://newyork.public.law/laws/n.y._cooperative_corporations_law_section_85)). The same article says a worker co-op is governed by the corporation law ([§ 84](https://newyork.public.law/laws/n.y._cooperative_corporations_law_section_84)), so a court could read it the other way, but the ten-largest rule isn't among the corporation-law sections it brings back. I haven't found a court case either way. "Wages" includes severance, vacation pay, and benefit and pension contributions. A worker has to give written notice within 180 days after the work ends, and can sue a member only after a judgment against the co-op goes unpaid. A member who pays more than their share can recover the excess from the other members. Choosing another form doesn't escape it: in a co-op of ten or fewer equal members, the ten-largest rule reaches everyone anyway, and both rules apply to companies formed in other states when the work is done in New York.
|
||||
- **Massachusetts reaches officers and managers.** The president, treasurer and any officers or agents managing a corporation count as the employer under the state's wage law ([ch. 149, § 148](https://malegislature.gov/Laws/GeneralLaws/PartI/TitleXXI/Chapter149/Section148)), and unpaid wages are owed three times over, plus legal costs ([§ 150](https://malegislature.gov/Laws/GeneralLaws/PartI/TitleXXI/Chapter149/Section150)). Workers can't waive this. In a co-op, those officers are members the others elected, and they may be paid no more than anyone else.
|
||||
- **New Jersey's closing law may reach owners and managers.** Its severance duty, a week per year worked in large closings and layoffs, falls on the "employer," which it defines to include anyone acting in the employer's interest, anyone who owns and operates it, and whoever makes the decision behind the layoffs ([P.L. 2019, c. 423](https://pub.njleg.state.nj.us/Bills/2018/PL19/423_.HTM); [Protecting Members' Savings](savings-guarantee.md#notice-pay-and-severance)). In a co-op, the members own and operate it.
|
||||
- **California reaches owners and managers who cause it.** An owner, director, officer or managing agent who violates certain wage rules, "or causes" them to be violated, can be held liable as the employer ([Labor Code § 558.1](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=LAB§ionNum=558.1)). That includes the penalty for final pay not paid on time, which keeps running as a day's wages per day for up to 30 days ([§ 203](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=LAB§ionNum=203)). "Causes" probably means some personal part in the decision, so officers who decided to keep people working are at risk, and a member who only voted probably isn't.
|
||||
|
||||
So the model does four things:
|
||||
|
||||
- **No working on credit.** A payroll counts as missed when wages are still unpaid five business days after the day state law says they're due. The board declares it and tells the pool the same day, and the pool can declare it if the board doesn't. From then on, the co-op can't schedule more ordinary work until everyone is paid up, unless it files for bankruptcy protection or the federation's fund lends it the payroll. The fund decides on a loan within two business days, and its loan is repaid after workers' wage claims, not before. Work to keep people safe, care for people or animals, save perishable goods and wind down in order goes on, and the pool pays for it first. This keeps a failing co-op from running up unpaid wages its own members might have to cover ([When Work Runs Short](downturns.md#when-cutting-isnt-enough)).
|
||||
- **Stopping and federal notice rules.** A sudden stop at a co-op with 100 or more workers can count as a plant closing that needed 60 days' notice ([29 U.S.C. § 2102](https://www.law.cornell.edu/uscode/text/29/2102)). The law shortens that when the business was seeking money that notice would have scared off, or when the cause wasn't foreseeable, but the employer must still give as much notice as it can. So a co-op whose quarterly accounts show the pool's warning signs ([Keeping Workers' Savings Safe](savings-guarantee.md#who-runs-it-and-how-claims-are-paid)) gives conditional notice early rather than waiting for the missed payroll.
|
||||
- **The insurance pool pays first.** The [pool](savings-guarantee.md#the-insurance-pool) covers up to eight weeks of each worker's unpaid wages, then notice pay ([Notice pay and severance](savings-guarantee.md#notice-pay-and-severance)). If the rule above is kept, that covers most unpaid wages, but notice pay, vacation, severance and benefit contributions can go past it. The pool doesn't pursue ordinary members for what it pays, and the co-op's bylaws and officers' insurance cover elected officers who kept the rule and didn't hide losses. Federal and Massachusetts liability can't be waived, but it can be insured.
|
||||
- **The model state law narrows it.** In states that adopt it, a member of a locked co-op covered by the pool isn't personally liable just for being a member, for claims by other members or for anything the pool paid. Workers who aren't members keep their full claims for anything the pool didn't pay, since they had no vote. There's no shield for wages earned after a payroll was missed and the rule wasn't kept, or for anyone who ran payroll or kept people working ([The Lock in State Law](lock-state-law.md#what-the-model-law-does), point 10). It doesn't touch federal law.
|
||||
|
||||
Until a state adopts that, members in states like New York take on a real personal risk. The risk follows where the work is done, not where the co-op was formed, and the bank's checked entry for each state says what it is. Every member is told in writing when they join, with the rules of the state where they work.
|
||||
|
||||
## Unemployment insurance: covered in most states, not all
|
||||
|
||||
Federal unemployment tax treats a corporation's officers as employees. But each state runs its own unemployment insurance, and several write owners or officers out of it ([US Department of Labor, *Comparison of State Unemployment Laws*, 2021, table 1-7](https://oui.doleta.gov/unemploy/pdf/uilawcompar/2021/coverage.pdf)). Examples:
|
||||
|
||||
- **New Jersey** excludes corporate officers and anyone who owns at least 5% of the business. In a co-op with fewer than 20 members, every member owns more than 5%.
|
||||
- **Minnesota and Wisconsin** limit benefits for someone who owns 25% or more. In a co-op of three or four founders, every founder does.
|
||||
- **Washington and South Carolina** leave corporate officers out unless the corporation chooses to cover them. Members often serve as officers.
|
||||
|
||||
For a member who loses their job when the co-op fails, that's the worst time to find out they're not covered.
|
||||
|
||||
**Washington has partly fixed it.** Since 2024, officers of worker co-ops there no longer lose benefits for owning 10% or more of the company, and serving only as an officer doesn't count as working ([Laws of 2023, chapter 92](https://lawfilesext.leg.wa.gov/biennium/2023-24/Pdf/Bills/Session%20Laws/Senate/5176-S.SL.pdf)). The co-op still has to choose to cover its officers ([RCW 50.04.165](https://app.leg.wa.gov/RCW/default.aspx?cite=50.04.165)).
|
||||
|
||||
**The model's rules:**
|
||||
|
||||
- **Always cover members.** Where a state lets a co-op choose whether to cover its officers or owners, a locked co-op chooses to cover them. That's a condition of the bank's loan and part of the charter.
|
||||
- **Members are told in writing** before they join if their state leaves them out, the same notice Washington already requires for officers who aren't covered.
|
||||
- **A model state law copies Washington and goes further.** It's a separate short law a state can adopt on its own, apart from [the lock's state law](lock-state-law.md): members of a locked co-op are covered like any other worker, and their membership share doesn't count as ownership when they claim.
|
||||
- **Owners can't lay themselves off in turns.** The worry behind the state exclusions is real: owners can arrange their own layoffs. So under the model law, a member laid off in a rotation isn't counted as unemployed, except through the state's work-sharing program where it has one. Layoffs follow the co-op's [written layoff order](downturns.md#the-layoff-order), and like any employer, a co-op whose members claim benefits pays a higher tax rate.
|
||||
- **Where a state still excludes members,** co-ops in that state can band together through the [federation](federation.md) for their own unemployment cover. Mondragon did this. When Spain excluded co-op members from its social security system in 1958, the co-ops built their own mutual insurer, Lagun Aro, and later added an unemployment benefit ([MONDRAGON, *History*](https://www.mondragon-corporation.com/en/history/)). In America, a shared fund like that is insurance, so it has to be licensed by the state or set up as a special trust the tax law allows for unemployment pay. It's optional.
|
||||
- **The pilot counts the gap.** It publishes how many members, by state, had no unemployment cover, and what happened to them when they lost their jobs.
|
||||
|
||||
## Workers' compensation: covered, unless someone opts out
|
||||
|
||||
Injury insurance usually covers corporate officers and directors who work for pay, but many states let owners opt out. California, for example, covers officers and board members "while rendering actual service for the corporations for pay" ([Cal. Labor Code § 3351(c)](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=LAB§ionNum=3351)). But an officer or director who owns at least 10% of the company and has health insurance can sign a waiver ([§ 3352(a)(16)](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=LAB§ionNum=3352)). In a co-op of ten or fewer members, every member owns that much, and the board is made of members.
|
||||
|
||||
**The model's rule: nobody opts out.** Every member, officer and board member who works for the co-op is covered, as a condition of the loan. Opting out saves a little on premiums and leaves an injured worker with far less.
|
||||
|
||||
The same goes for state paid sick leave and paid family leave where a state lets owners or officers opt out. The charter's own leave protections ([Does Sharing the Profit Mean Less Effort?](free-riding.md#when-a-member-wont-do-the-work)) cover small co-ops that federal family leave law, which starts at 50 workers, doesn't reach.
|
||||
|
||||
## Anti-discrimination law: probably covered, and the charter covers the rest
|
||||
|
||||
Federal anti-discrimination laws protect employees, not owners. In 2003 the Supreme Court said the question is control: does the person "act independently and participate in managing the organization," or are they "subject to the organization's control"? It listed six factors, including whether the organization can fire them, whether they report to someone, and whether the parties intended them to be an employee. It also said a job title or a written agreement doesn't settle it ([*Clackamas Gastroenterology Associates v. Wells*, 538 U.S. 440](https://www.law.cornell.edu/supct/html/01-1435.ZO.html)).
|
||||
|
||||
A co-op member who reports to a manager and can be dismissed looks like an employee on most of those factors. **The standard membership agreement says in writing that members are employees of the co-op for every employment law.** That's one factor, not a guarantee, but it's the one the co-op controls.
|
||||
|
||||
The main federal laws don't apply to employers with fewer than 15 workers (20 for age discrimination), and many co-ops will be that small. Many state laws reach smaller employers. California's, for example, starts at five ([Cal. Gov. Code § 12926(d)](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=GOV§ionNum=12926)). Below that, the charter does the work. The dismissal process bars discrimination and retaliation for every member, whatever the co-op's size ([Does Sharing the Profit Mean Less Effort?](free-riding.md#when-a-member-wont-do-the-work)). The charter extends the same ban to every job decision: hiring onto probation, pay, schedules and harassment. None of it replaces a worker's right to go to a government agency or a court. The charter says so, and it bars any agreement that would make a worker give that up.
|
||||
|
||||
## Union rights: the biggest gap
|
||||
|
||||
**The federal labor relations law (the NLRA) probably doesn't protect members' own union activity.** It protects employees, and the labor board has long excluded worker-owners who, as a group, control the company. In a 2002 case about a taxi company owned by its drivers, the board found the drivers weren't protected employees, because together they could elect and remove the board, officers and committees, repeal work rules, and change the company's constitution (*Citywide Corporate Transportation*, 338 NLRB 444). They also worked alongside employees who weren't owners, whose interests could differ. That's a close description of a locked co-op.
|
||||
|
||||
The board has gone the other way when *everyone* who works there is an owner and the owners' interest as workers is "at least as great" as their interest as owners, as in a 1953 case about a plywood co-op (*Everett Plywood & Door*, 105 NLRB 17). But the model allows up to a quarter of hours to be worked by people who aren't members yet. So members of a locked co-op would most likely fall on the unprotected side. The cases are collected in [Levinson, "Union Co-ops and the Revival of Labor Law," *Cardozo Journal of Conflict Resolution* vol. 19, 2018](https://static1.squarespace.com/static/60a5863870f56068b0f097cd/t/60a5e69e188d6e641ff01621/1621485214238/Union-Co-ops-and-the-Revival-of-Labor-Law.pdf).
|
||||
|
||||
**What that means in practice:**
|
||||
|
||||
- **Workers who aren't members are fully protected.** People on probation, and anyone else who isn't a member, can organize, and the co-op can't punish them for it. Members wouldn't be in their bargaining unit.
|
||||
- **A co-op can still recognize a union voluntarily.** Nothing stops a co-op from signing a contract with a union that covers everyone, members included, as Cooperative Home Care Associates in the Bronx has done with 1199SEIU ([When People Don't Stay Long](high-turnover.md#unions-stay)). The difference is that if a co-op fired a member for union activity, the labor board probably couldn't step in.
|
||||
|
||||
**The model's rules:**
|
||||
|
||||
- **The charter protects members who organize,** and members can't vote it away. It already bars dismissing a member within 12 months of organizing unless the co-op proves the reason was something else ([Does Sharing the Profit Mean Less Effort?](free-riding.md#when-a-member-wont-do-the-work)). When a member says they were dismissed for organizing, the case goes straight to an outside arbitrator from the federation's list, paid for by the bank, not to a hearing of the same members they were organizing against. If the dismissal is overturned, the member gets their job back with back pay.
|
||||
- **Recognition sticks.** Once a co-op recognizes a union, it can withdraw recognition only if a majority of the workers the union covers, members and non-members together, vote for that.
|
||||
- **A short change in federal law, as its own bill.** It amends the NLRA's definition of "employee" to say that membership in a worker co-op, and the powers members hold together, don't by themselves take a worker outside the law. Members who actually supervise others are still excluded, like supervisors anywhere. It's kept out of the bank's law, so a fight over labor law can't sink the pilot.
|
||||
|
||||
**The change needs one more piece.** The NLRA forbids an employer from dominating or funding any "employee representation committee or plan" that deals with it over pay or working conditions ([29 U.S.C. 152(5)](https://www.law.cornell.edu/uscode/text/29/152), [158(a)(2)](https://www.law.cornell.edu/uscode/text/29/158)). That rule exists to stop company unions. But once members count as employees, a co-op's member meetings and elected committees, which deal with pay and working conditions all the time, could look like exactly that. So the same bill says a worker co-op's own governance isn't a company union, and it protects members only from being punished for organizing. It doesn't force a co-op to bargain with a union of members alone.
|
||||
|
||||
## Other laws that apply
|
||||
|
||||
- **Workplace safety law covers members,** including its protection against being punished for reporting a hazard. The charter's retaliation rule covers safety complaints too.
|
||||
- **Health insurance.** The federal rule requiring employers to offer health coverage starts at 50 full-time workers, so it won't reach most co-ops. Members can use the public marketplace, and the federation can arrange a group plan.
|
||||
- **The buy-in doesn't come out of pay** unless a member asks in writing. It comes out of profit shares ([When People Don't Stay Long](high-turnover.md)), which keeps co-ops clear of state laws that limit deductions from wages.
|
||||
- **Workers without work authorization can't be members,** as [How a Locked Co-op Is Taxed](taxes.md) explains. Every co-op still owes every worker the minimum wage and a safe job, and the charter bars threats about anyone's immigration status.
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **The charter is weaker than law.** A member fired for organizing gets an outside arbitrator, not the labor board. Until the NLRA is changed, that's the protection.
|
||||
- **The NLRA change is new federal law,** and it may never pass. Opponents will say it's odd for owners to organize against themselves. The answer is that a member has one vote among many, and the managers they elect can still treat them badly.
|
||||
- **State unemployment law changes slowly.** Until states adopt the model law, members in some states have no unemployment cover unless their co-ops set up their own. Making that cover required would raise costs only for co-ops in those states. The pilot leaves it optional and counts who went without.
|
||||
- **The founding setup hasn't been tested.** Whether unpaid work before the co-op is formed is really outside wage law is a question for the Labor Department's opinion letter, and whether members' accounts stay outside pension law is another ([Member Capital Accounts](capital-accounts.md#why-not-pay-it-out-when-you-leave)). Until it answers, the bank's lawyers decide, and they could be wrong.
|
||||
- **Workers without papers are still left out** of membership, and so out of the profit share.
|
||||
- **This is a map, not legal advice.** State rules differ and change, and the Department of Labor's table is from 2021. Instead of every co-op paying a lawyer, the bank keeps a checked set of rules for each state and runs a shared payroll service that handles minimum wage, overtime and insurance.
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: The Lock in State Law
|
||||
section: The Co-op
|
||||
order: 31
|
||||
summary: The lock is written into each co-op's charter, but ordinary state law still leaves several ways out, and this page drafts a short law any state can adopt to close them for co-ops that choose the lock.
|
||||
---
|
||||
|
||||
# The Lock in State Law
|
||||
|
||||
[The lock](the-lock.md) is built from a co-op's own founding documents and a single *lock share* held by an independent lock-holder, which can veto a sale or a change to the lock. Most of that works under today's law. But co-ops and companies are creatures of state law, and state law has its own ways out that a charter can't fully close by itself.
|
||||
|
||||
This page lists those ways out and drafts a short model law that closes them. It applies only to co-ops that choose the lock. States can adopt it or not, and nothing in the federal law requires them to. A co-op in a state that hasn't adopted it is still locked by its charter and lock share, just less securely.
|
||||
|
||||
## Where state law leaks
|
||||
|
||||
- **Members can vote to stop being a co-op.** Massachusetts has had a law for worker co-ops since 1982, and a co-op can leave that status "by a vote of two-thirds of the members" ([Mass. Gen. Laws ch. 157A, § 4](https://malegislature.gov/Laws/GeneralLaws/PartI/TitleXXII/Chapter157A/Section4)). That's exactly the exit the lock closes.
|
||||
- **Mergers and changes of form usually need only the owners' vote.** In Delaware, for example, a merger is approved by "a majority of the outstanding stock" entitled to vote ([Del. Code tit. 8, § 251(c)](https://delcode.delaware.gov/title8/c001/sc09/index.html)). Most states also let a company turn itself into a different kind of business, or move to another state, by a vote. A lock share catches these only if the charter says its consent is needed, and only as far as each state's law lets a charter say so.
|
||||
- **Some protection already exists, unevenly.** Delaware already gives a class of shares its own vote on any change that would "alter or change the powers, preferences, or special rights of the shares of such class so as to affect them adversely" ([Del. Code tit. 8, § 242(b)(2)](https://delcode.delaware.gov/title8/c001/sc08/index.html)). So a Delaware lock share can block its own removal. Co-op laws often don't have classes of shares at all, and they vary a lot from state to state.
|
||||
- **Courts can end a company.** Massachusetts, like most states, lets a court dissolve a corporation when its directors or owners are deadlocked, or when its creditors can't be paid ([Mass. Gen. Laws ch. 156D, § 14.30](https://malegislature.gov/Laws/GeneralLaws/PartI/TitleXXII/Chapter156D/Section14.30)). After debts are paid, what's left normally goes to the owners. A charter can say otherwise, but whether a court has to follow that when it's the one dissolving the company isn't settled everywhere.
|
||||
- **Courts dislike permanent limits on selling.** A lock is a "restraint on alienation," a limit on selling property, and courts look hard at those when they last forever ([Cornell Legal Information Institute](https://www.law.cornell.edu/wex/restraint_on_alienation)). Limits on selling company shares are usually allowed if they're reasonable, but "reasonable" is up to a judge.
|
||||
- **Creditors can sell without a bankruptcy court.** A lender with a claim on a business's equipment and other assets can sell them after a default, or keep them, under state law ([UCC § 9-610](https://www.law.cornell.edu/ucc/9/9-610)). A mortgage lender can foreclose on its building. A creditor with a court judgment can have the sheriff sell its property, a tax agency can hold a tax sale, a state court receiver can sell the business, and so can an assignee when a business hands everything over to pay its creditors. The [bankruptcy amendment](bankruptcy.md#what-can-be-sold-and-to-whom) tries to reach those sales with a federal law resting on Congress's power over commerce, which will be challenged. A state law covering the same sales is on much firmer ground.
|
||||
|
||||
## What the model law does
|
||||
|
||||
**1. Locking is a choice, and people who vote no can leave.** A co-op becomes locked by saying so in its articles, naming a lock-holder from the approved list and issuing the lock share. For a new co-op, the founders decide it unanimously. An existing co-op or business, like one being [sold to its workers](buyouts.md), needs at least a two-thirds vote of its members, and never less than the vote it would take to leave co-op status. Members who vote no can leave within 90 days and have their whole capital account paid out on the normal schedule. They aren't paid what the business would sell for unlocked. That value never belonged to any one member, and paying it out is exactly the windfall the lock exists to prevent. The status is shown in the state's public business records, and a notice is filed where lenders already look for claims on a business's property (a UCC filing).
|
||||
|
||||
**2. The members' exit doesn't apply.** Once locked, a co-op can stop being a co-op only with the lock-holder's consent, and the lock-holder can consent only to two things: merging into another locked co-op, or closing with its remaining value going to the co-op movement's fund. Any member vote that a state's co-op law would otherwise allow, like the Massachusetts two-thirds vote, doesn't apply to a locked co-op. The lock clauses in the articles, the named fund and the lock-holder can't be changed without the lock-holder's consent either. This is the one real change to members' rights, and it's the point of the law.
|
||||
|
||||
**3. The lock is valid by law.** The lock share and the lock's limits on selling are declared reasonable and not an unlawful restraint on alienation. Anyone who takes over the business with notice of the lock is bound by it, including a receiver, a trustee or a buyer. Notice comes from the public record and the UCC filing.
|
||||
|
||||
**4. The lock-holder has rules too.** Without its consent, a locked co-op can't merge, convert or move to another state except into another locked co-op, and can't sell, lease or license more than a quarter of its assets or its revenue-producing business within any three years, counted together, or its brand, core technology or main customer contracts, whatever their size ([Closing the back doors](the-lock.md#closing-the-back-doors)). Ordinary sales, like selling stock to customers or replacing old equipment with new, don't count. The quarter is measured on the co-op's books at the start of the three years. Companies the co-op controls are locked too. But the lock-holder isn't free to say yes or no as it likes:
|
||||
|
||||
- **It must say yes** to a sale at a fair price of things the co-op no longer needs, and to spinning off part of the business as a new locked co-op.
|
||||
- **It must say no** to anything that would leave most of the business, its brand, technology or main contracts in the hands of anyone but a locked co-op.
|
||||
- **Silence means yes.** If it hasn't answered a request it must approve within 30 days, the answer is yes.
|
||||
- **It can be challenged and replaced.** Members can take a refusal to court quickly, and the lock-holder pays their costs if the refusal was unreasonable. Members can also replace their lock-holder by a two-thirds vote, but only with another one on the approved list, and not in the middle of a deal it's reviewing.
|
||||
|
||||
**5. Breaking the rules doesn't pay.** The state's business filing office must refuse any filing for a merger, conversion, move or change to the lock clauses that breaks them. Asset sales aren't filed with the state, so for those the remedy is different. A sale that breaks the lock can be undone against a buyer who knew about it. A buyer who paid a fair price without knowing keeps what it bought, but the co-op's gain from the sale goes to the fund, and the insiders who arranged it are liable for the rest. That protects honest buyers, which lenders and buyers need before they'll deal with a locked co-op at all.
|
||||
|
||||
**6. Closing hands the value on, even when a court does the closing.** When a locked co-op closes, by its own vote or by a court's order, what's left after its debts and the members' own account balances ([Member Capital Accounts](capital-accounts.md)) goes to the co-op movement's fund named in its articles. Balances count only as allocated under the co-op's published rules. Anything added to members' accounts in the two years before closing, above the average of the three years before that, goes to the fund instead, so members can't empty the reserve into their own accounts on the way out. If the named fund no longer exists, the lock-holder names the closest one, and the attorney general does only if the lock-holder can't. Courts keep every power they have to dissolve a company and to protect members who've been treated unfairly, paid from their own accounts. What they lose is the power to hand the rest to the members.
|
||||
|
||||
**7. Sales outside bankruptcy follow the same rules as inside it.** The rules are written into the state law itself, so they don't depend on a federal law that might be changed or never passed. They apply when a lender sells or keeps a locked co-op's property, a mortgage is foreclosed, a sheriff sells for a court judgment, a tax sale is held, a receiver sells, or the co-op hands everything over to pay its creditors, if what's sold adds up, over three years, to more than a quarter of the business, or includes its brand, technology or main contracts. They're the rules in [When a Locked Co-op Goes Bankrupt](bankruptcy.md#what-can-be-sold-and-to-whom):
|
||||
|
||||
- for 90 days, only other locked co-ops, the federation and a co-op formed by the workers can buy the business whole, at a price no lower than what the pieces would bring, set by an independent appraiser;
|
||||
- after that, the pieces can go to anyone, but no buyer can put the business back together, and a buyer who hires more than a third of the members within two years owes the difference to the co-op's creditors;
|
||||
- creditors who never chose to lend, like people the co-op injured and tax agencies, are paid by the federation fund anything the lock cost them, as in bankruptcy.
|
||||
|
||||
A lender that follows these rules has sold in a "commercially reasonable" way under the UCC, so it can't be penalized for the lower price. A clause in a loan agreement choosing another state's law to avoid these rules is void in the adopting state's courts. A lender whose claim was on file before the co-op locked, and who didn't agree to the lock, isn't bound.
|
||||
|
||||
**8. Several parties can enforce it.** The lock-holder, any member, any other locked co-op, the bank, and the state attorney general can all go to court to enforce the lock. That matches [The Lock](the-lock.md#how-the-lock-is-made-to-hold). Anyone who brings a frivolous case pays the other side's costs, as in [bankruptcy](bankruptcy.md#everything-else), so competitors can't use lawsuits to hold up each other's deals.
|
||||
|
||||
**9. A backstop if the lock breaks anyway.** If a court still lets a locked co-op convert or be sold, the value above creditors and members' own balances goes to the fund, not to the members. That's layer 4 of the lock, now written into state law. Nobody gets a windfall from breaking it. This part has real precedent. Italian co-ops that take the tax benefits for being mainly mutual must write into their bylaws that their whole net value, apart from members' paid-in capital, goes to the movement's funds when they close ([Italian Civil Code, art. 2514](https://www.brocardi.it/codice-civile/libro-quinto/titolo-vi/capo-i/sezione-i/art2514.html)), and the same happens if they become an ordinary company ([art. 2545-undecies](https://www.brocardi.it/codice-civile/libro-quinto/titolo-vi/capo-i/sezione-v/art2545undecies.html)). Italy has sent that value to the funds since 1992 ([Law 59/1992, art. 11](https://www.diritto.it/i-fondi-mutualistici-per-la-promozione-e-lo-sviluppo-della-cooperazione/)). But Italy lets a co-op change its form. It locks the value, not the co-op, and that's the softer alternative [The Lock](the-lock.md#what-the-lock-costs) leaves open.
|
||||
|
||||
**10. Members aren't wage guarantors just for being members.** Some states make a company's owners personally liable for its unpaid wages, and New York makes every member of a co-op liable ([Owners Are Still Workers](labor-law.md#when-wages-go-unpaid-members-can-owe-them)). In a locked co-op, those owners are its workers. So a member of a locked co-op that's covered by the insurance pool isn't personally liable just for being a member, for claims by other members or for wages the pool paid. Workers who aren't members keep their full claims for the rest. People who ran payroll or kept others working, whatever their title and even if they stepped down in the 90 days before, stay liable as the state's law provides, and so does anyone paid in full, or paid anything else by the co-op, while others went unpaid. The shield doesn't cover wages earned after a missed payroll if the co-op kept working on credit. Federal law isn't affected.
|
||||
|
||||
**What it leaves alone.** It changes no taxes, sets no rules for co-ops that didn't choose the lock, and doesn't require any co-op to lock. Everything else about running a co-op stays with the state's ordinary co-op or corporate law.
|
||||
|
||||
## Which state's law counts
|
||||
|
||||
Under a long-standing rule, a company's internal affairs, like who votes on a merger, whether members can leave co-op status, and where its value goes when it closes, are governed by the law of the state where it's organized, not where it does business. So a co-op can organize under the model law in a state that has adopted it, wherever it works, the way many companies organize in Delaware. The bank lends to co-ops organized anywhere, but it publishes which states have adopted the law.
|
||||
|
||||
That rule covers less than it sounds like:
|
||||
|
||||
- **Property is different.** Sales of land follow the law of the state where the land sits. A receiver or sheriff in another state follows that state's law. Lenders' rules for equipment may too.
|
||||
- **Lenders pick their law.** A loan agreement can choose which state's law governs it ([UCC § 1-301](https://www.law.cornell.edu/ucc/1/1-301)). The model law voids a choice made to avoid the lock, but only in the adopting state's own courts.
|
||||
- **Some states reach in anyway.** California applies some of its own corporate rules to companies organized elsewhere that do most of their business there and are mostly owned by people who live there ([Cal. Corp. Code § 2115](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=CORP§ionNum=2115)).
|
||||
|
||||
Outside the adopting state, the co-op's charter, the lock share and the federal rules are what's left.
|
||||
|
||||
## Draft text
|
||||
|
||||
A short version, written to be dropped into a state's co-op or corporate law. Section numbers are placeholders.
|
||||
|
||||
> **Sec. 1. Definitions.** (a) A "locked cooperative" is a cooperative or corporation whose articles (1) state that it is a locked cooperative under this Act, (2) name a lock-holder approved under Sec. 9, and (3) name a cooperative development fund. (b) The "lock share" is the single share issued to the lock-holder. (c) A "lock-preserving transaction" is a merger or consolidation into, or a transfer of the business to, another locked cooperative, or a dissolution under Sec. 6. (d) A "covered transfer" is any transfer, lease or license, voluntary or not, that together with others in the preceding 36 months exceeds 25 percent of the book value of the cooperative's assets at the start of that period, or 25 percent of its revenue-producing business, or includes its principal trade name, core technology or principal customer contracts, but not a sale of inventory or replacement of equipment in the ordinary course of business.
|
||||
>
|
||||
> **Sec. 2. Election.** (a) A cooperative becomes a locked cooperative by adopting articles under Sec. 1(a), approved (1) at formation, by all of its founding members; or (2) otherwise, by at least two-thirds of its members and not less than the vote required to revoke cooperative status, and by the holder of any security interest in substantially all of its assets. (b) A member who voted against an election under subsection (a)(2) may withdraw within 90 days and shall be paid the balance of the member's capital account on the cooperative's ordinary schedule. (c) The Secretary of State shall show locked status in the public record of the entity, and the cooperative shall file a financing statement giving notice of it.
|
||||
>
|
||||
> **Sec. 3. Status.** (a) A locked cooperative may end its locked status only by a lock-preserving transaction. (b) Any provision of law allowing members to revoke cooperative status, including [the state's worker-cooperative revocation section], does not apply to a locked cooperative. (c) Without the written consent of the lock-holder, no amendment may change the articles' lock provisions, named fund or lock-holder, except a replacement under Sec. 5(d). (d) The lock share and the restrictions in the articles are reasonable restrictions and are not void as restraints on alienation. They bind every successor, receiver, assignee, trustee and transferee with notice.
|
||||
>
|
||||
> **Sec. 4. Restricted transactions.** Without the written consent of the lock-holder, a locked cooperative, and any entity it controls, shall not (1) merge, consolidate, convert, domesticate or transfer to another jurisdiction, except in a lock-preserving transaction or into an entity that remains a locked cooperative; or (2) make a covered transfer.
|
||||
>
|
||||
> **Sec. 5. Duties of the lock-holder.** (a) The lock-holder shall consent to a lock-preserving transaction, to a covered transfer of assets no longer needed in the business at fair value, and to a transfer of part of the business to a new locked cooperative. (b) The lock-holder shall not consent to any transaction by which all or substantially all of the business, or its principal trade name, core technology or principal customer contracts, would be held by anyone other than a locked cooperative. (c) A request under subsection (a) not answered within 30 days is approved. A member may seek expedited review of a refusal, and the court shall award costs against a lock-holder whose refusal was unreasonable. (d) Members may replace the lock-holder by a two-thirds vote with another approved lock-holder, except while a request is pending before it. (e) A lock-holder shall not act on a transaction in which it, or a fund it controls, is a party or would receive assets.
|
||||
>
|
||||
> **Sec. 6. Dissolution.** (a) On the dissolution of a locked cooperative, whether voluntary or by order of a court, the assets remaining after payment of creditors and of members' capital account balances shall be distributed to the cooperative development fund named in its articles, or, if that fund no longer exists, to a fund named by the lock-holder that most nearly serves the same purpose, or, failing that, by the Attorney General. (b) Allocations to members' capital accounts in the 24 months before dissolution, in excess of the average annual allocation in the 36 months before that period, are paid to the fund and not to the members. (c) No court shall order any other distribution of those assets, except payment from a member's own capital account.
|
||||
>
|
||||
> **Sec. 7. Transfers outside bankruptcy.** (a) Notwithstanding [the state's enactment of UCC Article 9], any law governing mortgages, execution, tax sales, receiverships or assignments for the benefit of creditors, and any choice of law in an agreement, a covered transfer of a locked cooperative's property by a secured party, mortgagee, officer executing a judgment, taxing authority, receiver or assignee shall be made as follows: (1) for 90 days after notice of the sale, the business may be sold as a whole only to a locked cooperative, a cooperative federation or a cooperative formed by its workers, at a price not less than the value of its assets sold separately as found by an independent appraiser; (2) thereafter its assets may be sold separately, but no buyer, with anyone financially related to it, may acquire all or substantially all of the business; and (3) a buyer under paragraph (2) that employs more than one-third of the cooperative's members within two years shall pay the cooperative's creditors the difference between the price paid and the value of the business as a going concern. (b) A disposition made in accordance with this section is commercially reasonable. (c) The cooperative development fund shall pay each involuntary creditor the amount, if any, by which this section reduced its recovery. (d) This section does not apply to a secured party that did not consent to the election and whose interest was perfected before the cooperative became locked.
|
||||
>
|
||||
> **Sec. 8. Enforcement and remedies.** (a) The lock-holder, any member, any other locked cooperative, [the bank] and the Attorney General may bring an action to enforce this Act. A court shall award costs against a party whose action is frivolous. (b) The Secretary of State shall not accept a filing that would effect a transaction prohibited by Sec. 3 or 4. (c) A transfer in violation of Sec. 4 may be set aside against a transferee with notice. Against a transferee that gave fair value without notice, it stands, and the cooperative's proceeds from it, and any loss to the cooperative, are recoverable for the fund from the persons who caused it. (d) If any conversion or transfer is completed despite this Act, the value of the business in excess of its debts and its members' capital account balances, as found by the court, is held in trust for the fund named in its articles.
|
||||
>
|
||||
> **Sec. 9. Lock-holders.** The Attorney General shall approve as lock-holders the organizations on [the bank's] list of approved lock-holders, and any cooperative federation or nonprofit that meets published standards of independence, funding and succession equivalent to that list's.
|
||||
>
|
||||
> **Sec. 10. Members' liability for wages.** (a) Notwithstanding [the state's laws making shareholders or members of a corporation, cooperative or limited liability company personally liable for its wages], a member of a locked cooperative that is covered by [the insurance pool established under the bank's authorizing act] on the date the unpaid services were performed is not personally liable, by reason of being a member, (1) to another member, or (2) to anyone else, to the extent of wages the pool has paid. (b) Subsection (a) does not apply to wages for services performed more than five business days after the cooperative first failed to pay wages when due, unless it was then operating under a filing for relief under title 11 of the United States Code or a payroll loan from [the fund]. (c) This section does not limit the liability of any person (1) who, at any time from 90 days before the cooperative first failed to pay wages when due, served as a director, officer or managing agent, approved payroll, signed for its payment or directed employees to continue work after wages were unpaid; or (2) who received wages in full, or any distribution, allocation paid in cash, redemption or other payment from the cooperative, for a period in which other employees' wages were due and unpaid. (d) This section does not affect liability under federal law.
|
||||
|
||||
## What it costs, and who objects
|
||||
|
||||
- **Members give up an exit the law otherwise gives them.** Libertarian and conservative critics object that the model law overrides a state's own rule, like the Massachusetts two-thirds vote, for people who may join the co-op decades after the choice was made. Founders choose it unanimously. In an existing business, those who vote no can leave with their savings. Anyone who joins later is told in writing before becoming a member. That narrows the objection but doesn't answer it: "forever" still binds people who weren't there, which is why the lock's [costs](the-lock.md#what-the-lock-costs) and the Italian alternative stay open questions.
|
||||
- **Members who vote no get their savings, not a share of the business.** A libertarian critic would pay them what their share would be worth if the business were sold unlocked. The model law doesn't, because that value is exactly what the lock keeps from being cashed out. So some dissenters will feel they lost something, and they may take it to court.
|
||||
- **The lock-holder has real power over big sales.** The duties above limit it, but a member who wants to sell part of the business still has to convince an outside body, or a judge.
|
||||
- **Lenders get less if a co-op fails.** Fewer buyers means lower prices, and a 90-day wait. Lenders will charge for that, and the bank's published recovery rates show how much.
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **Only adopting states.** A co-op organized elsewhere relies on its charter and lock share alone, and even a co-op organized in an adopting state can lose property sold under another state's law.
|
||||
- **A legislature can repeal it.** Nothing here can stop a later legislature changing its own corporate law, and most states reserve the power to amend the rules for companies they charter. A repeal would leave the charter, the lock share and the federal rules.
|
||||
- **Bankruptcy still needs Congress.** State law can't override federal bankruptcy law. The [bankruptcy amendment](bankruptcy.md) is still needed, and this law only covers the sales that happen outside bankruptcy.
|
||||
- **It hasn't been tested.** No US state has a lock like this for a business. The closest are charity law, which already keeps a charity's assets dedicated to its purpose, and Britain's community interest companies ([The Lock](the-lock.md#why-forever-isnt-as-strange-as-it-sounds)). How courts would read it, and whether a dissenter could win a claim that locking took their property, is unknown.
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Open Problems
|
||||
section: Guardrails
|
||||
order: 140
|
||||
summary: An honest list of what isn't solved yet.
|
||||
---
|
||||
|
||||
# Open Problems
|
||||
|
||||
An honest list of what the model hasn't solved yet, most dangerous first.
|
||||
|
||||
1. **Adverse selection on founders.** *(The most dangerous.)* The lock removes the exit, and profits are shared with every member. In the success case a co-op founder ends up with somewhere between about a seventh and a fortieth of what a conventional owner of the same firm would ([The Founder Problem](founder-problem.md)). For founders without wealth the gap in what the bet is *worth* is smaller, but at the top it's decisive. If the strongest founders go conventional, the bank's portfolio is the rejected pool, and the test fails for reasons that say nothing about co-ops. The pilot can at least tell how much of a result is down to founders: [Co-ops or Their Founders?](who-founds.md) measures founders on both sides from government records, and the verdict has to say how much their backgrounds explain. That doesn't change pass or fail, because if the lock keeps strong founders away, that's part of what the lock does. Still open: nothing yet makes the deal better for the founders the model most needs, and with about 400 co-ops, "too few to tell" is a likely answer.
|
||||
|
||||
2. **The horizon problem.** *(Mostly answered.)* Members near retirement have little reason to vote for investments whose payoff arrives after they leave. The evidence from France and Italy finds no underinvestment where co-ops must keep a reserve, and [Investing for the Long Run](the-long-run.md) closes the main routes around this model's reserve: pay above the going rate, sales of assets, development spending, high pay that turns into losses, and worn-out equipment still in use. A warning light shows when a co-op stops replacing its equipment, and big bets get an independent opinion before members vote. Still open: nothing forces a co-op to maintain what it has, a bad bet that fails after its backers leave falls on the people who came after, and it's unsettled whether 30% is enough and whether the account rate is right. Members' accounts now pay back within three to seven years ([Member Capital Accounts](capital-accounts.md#why-not-pay-it-out-when-you-leave)), so they help less with long-lived investments than accounts held until members leave. Letting members sell their places to incoming workers was rejected, because that is how the plywood co-ops ended up sold.
|
||||
|
||||
3. **Members' savings sit inside their employer.** *(Partly answered.)* [Member capital accounts](capital-accounts.md) are capped at a year's pay, protected by a solvency test, and now insured by a pool the co-ops pay for ([Protecting Members' Savings](savings-guarantee.md)). It covers final wages, former members' payouts and a protected slice of current accounts. Premiums are now estimated from Europe's wage guarantee funds and Census closure rates for small firms like co-ops: about 0.55% of payroll for final wages and notice pay, and 3% to 6.5% a year of covered savings depending on the co-op's age, which takes most or all of the interest on a protected slice. The wage rate assumes a failed co-op owes about five of the eight weeks, counting pay in place of notice, which nobody has measured. Notice pay and severance from overlapping federal, state and bylaw rules don't add up; a worker gets the largest of each, but the pool doesn't cover severance, and a long-serving worker's severance is mostly lost in a failure. The reserve target is six extra years' worth of claims while the pool is young, and limits for new additions fall when the reserve falls behind its planned path. If a recession hit in the pilot's fifth year, the pool would pay roughly 45 to 60 cents on the dollar of savings claims, so in its early years it's a partial promise, and cover for current members' savings waits until the pool has its own track record. Still open: the pool has no government backing, so a worse recession could outrun it; the estimates rest on rough data, not co-ops; the reserve takes about fifteen years to build; and it needs a federal charter to avoid fifty states' insurance laws. Also open: an account paid out when a member leaves would probably be a pension plan under federal law, which would forbid the co-op from using the money. So accounts now pay each year's credit back on a fixed schedule of three to seven years, whether or not the member has left. That's designed to stay outside pension law, but it isn't settled until the Labor Department answers, and federal appeals courts weigh programs like it differently. Because nobody loses their account for leaving, much of what's paid goes to people who've left, which weakens the case. So co-ops where more than a third of payouts go to people no longer working there shorten the wait, then stop making new credits, and credits taxed only when paid wait for the Department ([Member Capital Accounts](capital-accounts.md#why-not-pay-it-out-when-you-leave)). In high-turnover work, that almost always means no accounts, so the lowest-paid members lose the savings the model promises them. An ordinary retirement plan outside the business, with the co-op adding 3% of wages for everyone and, from 2027, a federal match for low earners, makes up part of that ([Member Capital Accounts](capital-accounts.md#saving-for-retirement)). It gives up the trust, disclosure and fiduciary rules that pension law would bring, which the model only partly replaces, and it leaves the co-op less of its members' money to invest for the long run. State wage laws are a second risk: in some states, California likely among them, a credit in a member's account could count as wages, which can't be held back, charged with losses or paused. Co-ops in those states don't use accounts until the state changes its law ([Owners Are Still Workers](labor-law.md#is-the-profit-share-pay)), so members there build no savings in the co-op. A third: account credits look like the notes the Supreme Court has held to be securities, so the model keeps members from putting money in and asks the SEC for an answer ([Member Capital Accounts](capital-accounts.md#are-accounts-securities)). If the answer is no, the nearest exemption means calling credits pay, which works against the model's positions on tax, pension and wage law.
|
||||
|
||||
4. **Is the lock too rigid?** A co-op that should be sold (a better operator exists, the members want out) can only go to another co-op. If the co-op sector is small, that buyer pool is tiny, and viable firms may be liquidated instead of rescued. The federation as buyer of last resort is a partial answer. Critics on the right add a sharper version: a lock that outlasts a repaid loan by generations is a permanent restriction on selling property, bought with one loan, and it binds members who never agreed to it. The lock is now built to hold in layers, including an independent lock share and rules against selling the business piece by piece ([The Lock](the-lock.md#how-the-lock-is-made-to-hold)). Its biggest legal gap is bankruptcy: federal bankruptcy law generally overrides limits on selling a bankrupt company's property, so the lock only holds in bankruptcy if Congress amends that law. [When a Locked Co-op Goes Bankrupt](bankruptcy.md) drafts that amendment. It's modeled on how bankruptcy law already treats charities. Only co-ops can buy a failed co-op whole, its workers can keep it running and bid with the wages they're owed, and creditors who never chose to lend are paid the lock's cost from the federation fund, which the co-op that buys the business repays. Whoever carries on the business, including members who keep it through a reorganization, takes on injuries from the old co-op's products, its warranties and household customers' deposits. When members keep a co-op over creditors' objections, the value faces a market test, though a weak one, since a conventional offeror can never win. That cost is usually nothing but can be large, and nobody has measured how often. The bank lends toward a workers' bid only up to what the assets would bring sold off piece by piece, so the rest has to come from the workers' claims, the insurance pool or patient money from the federation, and a failed firm often won't find it. It still needs Congress, a later Congress can repeal it, and while the co-op sector is small, some viable businesses will be broken up for lack of a co-op buyer. Italy offers a softer model: a co-op there can convert into an ordinary company, but its accumulated reserves go to the co-op development fund, not to the members ([Has This Been Tried?](precedents.md#italys-co-op-funds-coopfond-and-the-marcora-law)). That would keep the part of the lock that stops members cashing out what earlier generations built, and drop the part that stops a firm from ever changing hands.
|
||||
|
||||
5. **Scale.** *(Partly answered.)* Even a total success at pilot scale leaves most workers in conventional firms. The labor-market effect needs a sector large enough to move wages, and nobody knows how long growth to that size takes, if it happens at all. That's the price of not forcing it. [After a Pass: How Big, How Fast](scale.md) works through the numbers: even growing at twice the recent pace every year, co-ops would take about 19 years to reach 1% of today's private jobs. Growth after a pass comes in capped stages sized to demand the reviewers certify, and the labor-market effect gets its own test, by lottery, in local markets where co-ops could become a large share of one kind of work. Still open: the closest evidence cuts both ways (big retailers paying more didn't lift other employers' pay; hospitals hiring nurses did), and co-ops may never become a big enough share anywhere to find out. A first-draft power calculation from public pay records ([The numbers, in a first draft](scale.md#the-numbers-in-a-first-draft)) says the local test could only see an effect of about 5% of pay, bigger than the union evidence suggests a 10% co-op share could produce. So on public numbers the test fails its own rule and wouldn't run. Unless better records change that, the labor-market effect may stay untested. And as a way to raise pay across a whole kind of work, wage boards are faster, more direct and cheaper for the government's budget, though their effect on jobs is disputed ([Compared with wage boards and unions](scale.md#compared-with-wage-boards-and-unions)). The labor-market effect isn't a reason to prefer co-ops for that job.
|
||||
|
||||
6. **Political durability.** *(Partly answered.)* Gradual construction invites gradual repeal. [Making It Last](durability.md) builds the bank to survive a hostile government: the lending standards and the test are fixed in law, board vacancies can't freeze lending, the verdict says "not tested" rather than "failed" if the bank turns away applicants who meet its own standards (every rejection is re-scored, the line is more than 1 in 10 clear passes turned away, and the result has to be one those applicants could have changed; see [the line](durability.md#when-rejections-mean-not-tested)), and repeal can't unlock the co-ops. Still open: after the Supreme Court's 2026 ruling any president can replace the bank's board; money not yet lent can be taken back in a budget bill; the lock's bankruptcy protection depends on a federal law that can be repealed; and at pilot scale the sector has no real constituency. The 1-in-10 line is a judgment call with no benchmark behind it. The bank's law lets applicants and borrowers sue to enforce its deadlines and signed loans ([Who can go to court](durability.md#who-can-go-to-court)), but that's untested, lawsuits are slow, and if the Justice Department won't defend the law itself, the people left to defend it may not be allowed to. The Constitution probably requires that the board have the last word on each loan, so the board can overrule its staff, only in writing, by named vote and in public ([Who has the last word](durability.md#who-has-the-last-word)). Whether the credit officers can keep protection from being fired after *Slaughter* is unsettled, and a president who empties the board hands its power to the Treasury Secretary.
|
||||
|
||||
7. **Growth and dilution.** *(Partly answered.)* Softened by the tenure ramp, not eliminated, and federal tax law softens it less than the founding plan expected: by default, the part of a share new members haven't reached goes to the reserve, not to other members ([How a Locked Co-op Is Taxed](taxes.md#the-tenure-ramp)). In steady state, an equal split per head still means a marginal hire must generate the average surplus to break even for incumbents. Capital-intensive expansion into lower-margin lines will face internal resistance conventional firms don't have. [When Growing Means Sharing](growth.md) makes Mondragon's answer the standard path: a lower-margin line becomes its own locked co-op, funded from the parent's reserve as a finite, voteless investment, under rules that stop it being used to push existing workers out or move money through insider deals. Still open: growth that can't be split off still dilutes, spin-offs create richer and poorer co-ops (the groups can pool profits, but don't have to), and the evidence comes from co-ops that could hire non-members, so it probably understates the problem under the lock. Members who share profit also have a reason to hire people like themselves. The pilot tests co-ops' hiring as a group with fake paired applications ([Testing hiring directly](growth.md#5-measuring-it)). But that can only catch co-ops being much worse than ordinary employers, conventional firms aren't tested alongside them, and hiring through members' friends and family is only counted, not tested. And the measure of whether co-ops grow more slowly than similar firms is too noisy, at pilot scale, to find anything but a very large gap ([Measuring it](growth.md#5-measuring-it)).
|
||||
|
||||
8. **Who decides which sectors are "essential"?** *(Partly answered.)* Subsidy authority is price-setting power over a large chunk of the labor market, and every low-margin industry will suddenly discover it is Essential. [Who Decides What's Essential](essential-work.md) takes the choice away from people. A fixed formula decides: work qualifies only if public programs already pay for most of it and it pays badly. The money goes to workers by the hour, measured from records like Medicaid's visit logs, and states whose rates fall behind inflation lose federal Medicaid money in step. The data sources are now named: federal payment records for public money, and Census revenue, with new Census questions on a deadline and rules so status can't flip on noise. Still open: counted properly, hospitals are near the line, which would add their low-paid jobs and a great deal of cost; child care's status is undetermined until the new data exists; other work mostly paid for by families doesn't qualify; employers and owners may still capture some of the money; workers without work authorization get nothing and may be made worse off; and at a first-draft $3 an hour it costs about $16 billion to $20 billion a year for home care aides and nursing assistants alone, more if hospitals qualify ([Paid to workers, by the hour](essential-work.md#paid-to-workers-by-the-hour)).
|
||||
|
||||
9. **The 1/N problem.** *(Mostly answered.)* Peer monitoring works at 30 people; at 10,000 your extra effort raises the pool by a rounding error. The answer is culture and structure: co-ops really do push out free riders, and the federation model keeps units small enough for monitoring to bite while pooling risk above them. [Does Sharing the Profit Mean Less Effort?](free-riding.md) turns that into rules: small teams with their own numbers, team bonuses whose formula members set in advance, and a fair, fast dismissal process with protections against retaliation and discrimination. It sets first-draft numbers: all bonuses together at most 10% of pay and no one's more than 15% of their own, and a dismissal needs four of five members drawn by lot, with an outside panel from other co-ops for small co-ops. Still open: the evidence comes from conventional firms with profit sharing, not co-ops, and a small shared stake may even make people less willing to speak up; the pilot tracks value added per hour as co-ops grow to find out.
|
||||
|
||||
10. **The pilot's own weak points.** [The pilot](pilot-spec.md) puts the burden of proof on the co-ops, which means roughly a 1-in-10 chance of shutting down a pilot that was actually working. It tests individual co-ops, not the [federation](federation.md) support network Mondragon relied on. Its lotteries test help after the loan and, since the pre-launch lottery was added, help before it ([Help Before the First Loan](before-launch.md)), but only among groups that come forward, not outreach to workers who never think of starting a co-op. The pre-launch help now has a cost ceiling set in advance, $400,000 of total public cost per extra co-op still open at year four, but there's no good benchmark for it, and the lottery is small enough that "not settled" is a likely verdict ([The cost ceiling](before-launch.md#the-cost-ceiling)). Founders now put in a small stake they can pay in unpaid work, but founders who can't afford months of unpaid work still face a barrier. Its comparison group (small businesses with government-backed loans) is biased in directions that partly cancel out, by amounts nobody knows. And market-rate lending based on collateral may mostly reach founders who already have some wealth. Approval rates by race, gender and wealth are published so that shows up, but publishing a gap doesn't close it.
|
||||
|
||||
14. **Drift toward the safest co-ops.** *(Partly answered.)* A bank that lends only to co-ops can still lend mostly to the safe ones, like consultancies started by people with savings, as America's co-op bank did. [Who the Bank Lends To](who-gets-loans.md) sets aside $90 million of the loan money for co-ops in lower-paid work, half of it for the lowest-paid industries, which goes back to the general pool only if the reviewers certify the bank handled those applications fairly. Results are reported for that group and for first-time owners without much wealth, and the verdict has to say which kinds of co-ops it covers. Still open: the set-aside makes money available but can't make groups apply; within each half it can be used mostly on the better-paid end; "lower-paid" is judged by industry, which leaves out poorly paid co-ops in well-paid industries and can be gamed by how a co-op describes its work; first-time owners without wealth get reporting, not money; and in work whose prices are set by Medicaid or contractors, a failure may say more about the price than about co-ops. Even measuring who gets loans needs its own rule: lending rules bar asking applicants' race and sex, and the federal exception for small-business lenders won't reach a bank this small, so the bank's law has to require the question, and a later Congress could drop it ([Asking needs its own legal basis](who-gets-loans.md#treated-the-same)).
|
||||
|
||||
15. **Taxes.** *(Partly answered.)* A locked co-op is a corporation that pays tax once on members' profit shares, under the part of the tax code written for co-ops. [How a Locked Co-op Is Taxed](taxes.md) fits the model to it: profit is shared per hour worked, workers who aren't members get a share too, wages reach the going rate before any profit is shared, and charges come out of the current year's share before older, already-taxed balances. Still open: nobody has ruled that a co-op with an outside lock-holder and a reserve that goes to the federation counts as a co-op for tax purposes; whether profit shares owe payroll tax is unsettled; the ramp no longer shields senior members from new hires unless it's paid as wages; workers without work authorization can't be members; the kept reserve pays 21% even though nobody owns it; members can't take the 20% deduction owners of partnerships and S corporations get, so the co-op comes out ahead of partnerships only if profit shares escape payroll tax ([How this compares](taxes.md#how-this-compares-with-other-businesses)); the return on members' accounts probably can't be deducted, so account holders' returns are credited after the co-op's tax ([Interest on accounts](taxes.md#interest-on-accounts)); following the deferred-pay rules for lower-income members' untaxed credits could be read as conceding that the credits are pay; and a member who loses their account when a co-op fails can probably deduct only $3,000 a year of it, after being taxed on it in full, until Congress refunds that tax.
|
||||
|
||||
11. **Federal or state?** *(Mostly answered.)* The test stays federal, because one bank with one plan makes one clean experiment. Any state can copy the bank under the same published rules, reported separately, and a model state law protects the lock for co-ops that choose it ([The Lock in State Law](lock-state-law.md); [Making It Last](durability.md#federal-test-state-copies)). No state has adopted anything like it, and a co-op organized in a state that hasn't relies on its charter alone. Still open: whether a few state lenders sharing one plan would have been the stronger experiment after all, since the most durable public bank in America, North Dakota's, is a state bank.
|
||||
|
||||
12. **Voluntary buyouts: in or out?** *(Answered.)* In, on a separate track: $100 million of its own, healthy firms only, a willing seller, no new tax break, a capped price the seller shares the risk on, a vote of the whole workforce, and its own test of money and pay, which counts the tax sellers put off under an old tax break ([The tax break that already exists](buyouts.md#the-tax-break-that-already-exists)), so it can't flatter the startup result ([When an Owner Wants to Sell to the Workers](buyouts.md)). Still open: whether a price set by the market is fair to workers who pay it out of their own future work, and whether about 60 deals can show more than that the loans get repaid. The comparisons are now set out: SBA-financed purchases, ESOP sales and deals that fell through, with a loss rate fixed by formula in advance ([Who buyouts are compared against](buyouts.md#who-buyouts-are-compared-against)). With so few deals, the loss test can catch a track that does much worse than SBA lending, but it can't show that buyouts do better. Why co-ops can't simply use the SBA is now spelled out: its rules ask a co-op, unlike an ESOP, for cash up front and one member's personal guarantee. The bank's deals do without both, so they rest on the seller's paid-last share and a coverage test on past cash flow ([Why not just use the SBA?](buyouts.md#why-not-just-use-the-sba)). Under the SBA's rules from October 2026, those two rules are nearly the whole gap, and both are SBA policy it could change itself. If it does, or a small fund covers the cash stake, much of the track may not be needed. Whether that's enough in place of cash and guarantees is something only the loss record will show. State programs paid for by Treasury and USDA's rural guarantees already allow co-op buyouts in pieces. The track uses them, but can't count them as private money or use them to hide losses ([Other public help](buyouts.md#other-public-help-that-already-exists)). How often private lenders will really take an equal share is unknown, so deals the bank makes alone are capped at a third of the money. The help each sale needs, the workers' adviser, checks on deals that fall through and help after the sale, costs the public roughly $110,000 to $240,000 per closed deal, about $12 million in all. That comes out of the same margin the money test measures, so the track passes only if losses stay low ([What the help costs](buyouts.md#what-the-help-costs)).
|
||||
|
||||
13. **Work where people don't stay long.** *(Partly answered.)* In home care, most workers leave within a year or two. Membership is now automatic after a short probation, the buy-in comes out of profit shares rather than pay, and pushing people out before they'd join counts against the co-op ([When People Don't Stay Long](high-turnover.md)). Still open: with a five-to-ten-year profit share ramp, only a small minority of care workers ever reach a full share, so the people who stay collect much of the profit the others helped make. A much shorter ramp for low-margin, high-turnover co-ops would fix that, but it would go against the founding plan's range and weaken the ramp's protection against under-hiring. Also open: whether default members take part in governance, and whether Social Security counts a profit share as earned income for SSI, which decides whether members on SSI lose more by joining. SSI may also count an account credit as income when it's made. Until Social Security answers, members on SSI get their share all in cash and join only if they say yes ([How a Locked Co-op Is Taxed](taxes.md#benefits)), and workers who have only an IRS taxpayer number still can't be members. How labor law treats worker-owners now has its own page ([Owners Are Still Workers](labor-law.md)). Federal law makes the people who run payroll personally liable for unpaid minimum wages, and New York appears to make every co-op member liable for all unpaid wages, though no court has tested that. The model stops ordinary work after a missed payroll and asks states to narrow the rule ([When wages go unpaid](labor-law.md#when-wages-go-unpaid-members-can-owe-them)), but until a state does, members there carry the risk, and elected officers carry it everywhere unless insurance covers it. Federal labor law probably doesn't protect members' own organizing, so the charter does, and fixing the law takes a separate bill that may never pass. Some states leave members out of unemployment insurance, and whether founders' unpaid work before the co-op forms is outside wage law still needs the Labor Department's answer. A federal rule due in 2030 requires 80% of certain Medicaid home care payments to go to care workers' pay. Co-ops meet it from wages alone and pay any shortfall as wages, but in that work it leaves little room for profit shares or a reserve, and whether the rule survives is unknown ([When Medicaid decides how much goes to pay](high-turnover.md#when-medicaid-decides-how-much-goes-to-pay)).
|
||||
|
||||
16. **What the rules cost a small co-op.** *(Partly answered.)* Every check the model adds costs a small co-op money. [What the Rules Cost a Small Co-op](compliance.md) scales checking to size: a yearly review and a fixed list of tests instead of an audit for small co-ops, audits by lot and on warning signs, and a lock-holder review every two years. Even so, the model's own estimate is 1% to 2.5% of a small co-op's sales, which could be a large part of the profit in cleaning or home care. Whether lighter checking lets more cheating through is something the audits by lot will show. Italy's "false co-ops" are the warning.
|
||||
|
||||
*Not problems here, because nobody's company is forcibly bought:* valuing every firm in the economy, owners stripping value before a takeover, foreign capital flight, and years of payouts to former owners. The contractor boundary isn't an economy-wide problem either. Its co-op version, hiring non-members to dodge membership, is handled by [the lock](the-lock.md)'s membership requirement.
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: Who Sets the Pay
|
||||
section: The Co-op
|
||||
order: 22
|
||||
summary: Salaries in a co-op are set by its members, so the model guards against two opposite failures, a majority paying itself a bigger slice and pay squeezed so flat that higher-paid people leave, with open pay bands, a pay ratio each co-op chooses, and a second vote that protects the lower-paid.
|
||||
---
|
||||
|
||||
# Who Sets the Pay
|
||||
|
||||
In this model, base salaries still exist, and the co-op sets them. Hard jobs and in-demand jobs pay more ([Worker Ownership](worker-ownership.md)). There's no legal cap on pay.
|
||||
|
||||
That makes pay the one big lever the profit split doesn't control. Profit is divided by fixed rules: 30% to the reserve, then shares by hours worked, times each member's share weight, which grows with tenure until it's full ([Member Capital Accounts](capital-accounts.md)). Salaries are whatever the members decide. So a co-op's pay decisions can quietly undo the rest of the design, in two opposite ways.
|
||||
|
||||
## Two ways pay goes wrong
|
||||
|
||||
**Too much at the top.** A group of members who together make up a majority, say managers and skilled staff, can vote themselves higher pay. That money comes out of profit that would otherwise have been shared among all members. The model already takes part of it back. The reserve gets 30% of any pay above the going rate for the job, meaning the typical local pay for that kind of work ([Investing for the Long Run](the-long-run.md#the-rules)). But the other 70% still goes to whoever voted for it.
|
||||
|
||||
**Too flat, so higher-paid people leave.** Gabriel Burdín followed a random sample of Uruguayan workers through social security records from 1997 to 2010 ([Burdín, *Economic Journal*, 2016](https://gwern.net/doc/economics/2016-burdin.pdf)). He found:
|
||||
|
||||
- **Worker-run firms paid more at the bottom and less at the top.** Among the lower-paid (the 20th percentile), working in a co-op was linked with about 18% higher pay than in a conventional firm. Among the higher-paid (the 80th percentile), about 4% lower. That's a comparison across people, not proof that the co-op caused the difference.
|
||||
- **Their higher-paid members quit more.** Members paid above their own co-op's median were 3.7 times as likely to quit as those paid below it.
|
||||
- **Less squeezing meant fewer quits.** The effect was stronger in co-ops that squeezed pay more, and weaker among founding members.
|
||||
|
||||
That's one study, in one country. It measured only one of the two failures, since insiders quietly overpaying themselves is much harder to see in the data. So it doesn't show which failure is bigger. But it does show that the second one is real.
|
||||
|
||||
**Mondragon keeps a hard cap.** It started with top pay at three times the lowest, raised that to four and a half in the 1970s when it set up its own welfare system, and to six in 1988 in response to tax reforms. The head of its outreach program describes the cap as a balance between avoiding a brain drain and keeping a sense of fairness ([Co-operative News, 2025](https://www.thenews.coop/co-operative-approaches-to-pay/)).
|
||||
|
||||
## The rules
|
||||
|
||||
The model doesn't set anyone's pay. It makes sure pay is decided in the open, in the right order, and that the lower-paid can't simply be outvoted.
|
||||
|
||||
### Pay bands, not personal votes
|
||||
|
||||
- **Every job has a pay band**, a range from lowest to highest. Members approve the bands, as part of the pay policy.
|
||||
- **Managers place people within the bands**, and can give raises there, the way any employer does. Bonuses follow formulas the members approve in advance, within limits ([Does Sharing the Profit Mean Less Effort?](free-riding.md#team-bonuses-the-members-design)). Nobody's personal salary goes to a vote of the whole co-op.
|
||||
- **Offers can run for up to three years.** A salary agreed within an approved band holds for the length of the offer, even if the band changes later. So a co-op can hire someone with a firm offer.
|
||||
|
||||
### Everyone sees the bands
|
||||
|
||||
- **Every worker, member or not, sees every band** next to the going rate for that job. Where a job has fewer than three people in it, only the band is shown, not what each person earns. Managers' actual pay is always shown.
|
||||
- **Job ads list the pay band.** Several states already require this.
|
||||
- **Workers can always talk about their pay.** Federal labor law already protects that for most workers, and a co-op can't ask anyone to keep their pay secret.
|
||||
- **The public sees the totals.** Each year the co-op publishes its pay ratio, how much of its total pay was above the going rate, and pay by job, gender and race in summary form.
|
||||
|
||||
### Each co-op chooses its ratio
|
||||
|
||||
- **The bylaws must set a highest-to-lowest pay ratio.** There's no ready-made number. The founders have to pick one, and the founding documents show Mondragon's six to one for reference.
|
||||
- **The ratio counts everything and everyone.** It's measured on total pay per full-time hour, including bonuses and benefits valued at cost. It includes people working through agencies, contractors and related companies. Moving the cleaning to an agency doesn't raise the bottom of the scale on paper.
|
||||
|
||||
### Two majorities, not one
|
||||
|
||||
A majority vote can't protect a minority from the majority. So some pay decisions need **two majorities**: a majority of all members, and a majority of the members in the lower-paid half. They are:
|
||||
|
||||
- changing the pay ratio;
|
||||
- any band whose top is above the 90th percentile of pay for that job in the co-op's own area, for remote workers too, or above three times the co-op's own median pay. Where the survey gives no exact figure at the top, the highest figure it gives is used;
|
||||
- any change that raises bands for jobs held by more than half the members.
|
||||
|
||||
These votes are by secret ballot, with at least 30 days' notice. That makes both kinds of grab harder: a skilled majority can't vote itself a raise over the heads of the lower-paid, and a scarce specialist can still be paid what it takes, if both groups agree.
|
||||
|
||||
### Nobody votes alone on their own pay
|
||||
|
||||
- **The board approves managers' bands and the chief executive's pay.** Managers who sit on the board step out of those votes.
|
||||
- **No two people can set each other's pay.**
|
||||
- **Board pay, if any, is set by the members.**
|
||||
- **Founders' pay** follows the existing rule: the founding bylaws can set it for at most five years, then the members take over ([The Founder Problem](founder-problem.md#the-tools)).
|
||||
- **Pay is added up per person.** Pay from more than one role at the co-op, fees paid to a company the person owns, and pay from a related company all count together.
|
||||
|
||||
### Unions fit in
|
||||
|
||||
A union can bargain for any group of workers, members or not. For a minority that thinks it's being underpaid, collective bargaining is the standard protection, and it works inside a co-op too ([When People Don't Stay Long](high-turnover.md#unions-stay)).
|
||||
|
||||
## Why not a fixed cap
|
||||
|
||||
A legal cap on the ratio, like Mondragon's six to one, would be simpler. The model doesn't impose one, for two reasons:
|
||||
|
||||
- **Different businesses need different ratios.** A cleaning co-op and an engineering co-op don't hire in the same job market, and a cap that suits one would strangle the other.
|
||||
- **Pay squeezed too flat has a real cost.** The large study above found higher-paid members leaving co-ops that squeezed pay hardest. A national cap would make every co-op take that risk.
|
||||
|
||||
What the rules do instead is make every co-op choose a ratio, in the open, and make high pay something the lower-paid have to agree to.
|
||||
|
||||
## What the pilot measures
|
||||
|
||||
The [pilot](pilot-spec.md#what-gets-measured) publishes, alongside the gap between top and bottom pay:
|
||||
|
||||
- **the share of pay above the going rate**, so any drift toward insiders paying themselves shows;
|
||||
- **how often higher-paid members leave**, compared with the matched conventional firms, so a brain drain shows too;
|
||||
- **pay gaps by gender and race within the same job.**
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **The going rate is a rough measure.** A government wage survey can't tell a strong engineer from an average one, and it carries the same pay gaps as the rest of the economy. It's a yardstick for when to ask questions, not a measure of what work is worth.
|
||||
- **Two majorities are harder to reach.** Some fair raises will stall. That's the price of making sure the lower-paid can't be outvoted on pay.
|
||||
- **Open bands still reveal a lot in a small co-op.** Some people will find that uncomfortable, and some good hires may not want it.
|
||||
- **Nothing here makes a co-op pay its higher-paid people enough.** The rules make it easy to see when it doesn't. Whether members vote to pay what it takes to keep them is exactly what the pilot will show.
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: Guarding the Pilot
|
||||
section: Growing the Sector
|
||||
order: 66
|
||||
summary: The rules that stop people from gaming the pilot with sham co-ops, hidden owners, split-up borrowers, zombie firms, or a rigged review.
|
||||
---
|
||||
|
||||
# Guarding the Pilot
|
||||
|
||||
Any program that hands out money gets gamed. Venezuela's co-op boom produced tens of thousands of co-ops that existed mainly on paper ([YES! Magazine, 2007](https://www.yesmagazine.org/issue/latin-america/2007/05/12/venezuelas-co-op-boom)). Uruguay's FONDES sent nearly half its money to three politically connected firms ([El Observador, 2015](https://www.elobservador.com.uy/nota/mitad-de-dinero-del-fondes-fue-para-empresas-companeras--201572500)).
|
||||
|
||||
This page lists the ways someone could cheat [the pilot](pilot-spec.md), or sabotage it, and the rule that blocks each one. Most of these tricks don't require breaking any law. They only need a loophole, which makes them the likeliest threats. So the rules are written to close the loopholes.
|
||||
|
||||
## The hidden boss
|
||||
|
||||
**The trick.** A founder keeps the building, equipment, brand and software in a separate company they own. They sign up three friendly "members" and give the co-op a management contract or franchise deal with that company. The co-op borrows $3 million, and the money flows back to the founder as rent, fees and royalties. On paper it's a co-op. In practice it's the founder's business, funded by the public.
|
||||
|
||||
**The rules:**
|
||||
|
||||
- **Every deal with an insider is disclosed.** Any contract with a founder, manager, member's family, or a company they own is reported to the bank and checked each year to confirm it's priced like a deal between strangers. Overpayments are treated as a breach of the loan. After the loan is repaid, the same check continues as part of the lock, along with the two rules below on ending contracts and buying key assets ([Deals with insiders](the-lock.md#deals-with-insiders)).
|
||||
- **Members can fire management.** Any management or franchise contract can be ended by a members' vote on short notice. A contract the members can't get out of is a boss.
|
||||
- **The business owns what it needs to operate, or can buy it.** If key assets are leased from an insider, the bank gets a claim on them or the co-op gets an option to buy them at a fair price. That way the co-op can't be held hostage, and the bank's collateral is real.
|
||||
- **Membership is real.** Probation lasts at most 12 months, counted across rehiring, and membership is automatic when it ends unless the worker opts out through the lock-holder. Members must do at least half the work hours in the co-op's second year and three-quarters from its third, counting hours worked through agencies or contractors. Pushing people out before they'd join is tracked and counts against the co-op ([When People Don't Stay Long](high-turnover.md)). All of this is checked every year. A co-op that falls short counts as failed in the [pilot's survival measure](pilot-spec.md#what-gets-measured).
|
||||
- **Lying costs something.** False statements on a loan application mean immediate repayment, and the people who made them are personally liable.
|
||||
|
||||
## Draining it through "investors"
|
||||
|
||||
**The trick.** [Outside investors in a co-op](capital.md) get no vote and a claim that expires. But nothing so far limits how big or long that claim can be. A friendly investor could take 90% of the profits for 40 years, and the co-op would be a shell.
|
||||
|
||||
**The rules, for pilot borrowers:**
|
||||
|
||||
- **The bank gets paid first.** Any outside profit-sharing claim ranks behind the bank's loan.
|
||||
- **The bank must approve new claims.** Claims last at most 15 years.
|
||||
- **Insiders' claims count as insider deals.** An investment by a founder or their family is checked the same way as any other insider contract.
|
||||
|
||||
## Splitting one borrower into many
|
||||
|
||||
**The trick.** The limit is $3 million per co-op. So a promoter, franchisor or landlord sets up 30 "separate" co-ops, each run by a relative or employee, and collects $90 million: a quarter of the fund, controlled by one person.
|
||||
|
||||
**The rules:**
|
||||
|
||||
- **"Related" means economically tied, not just sharing names.** Co-ops count as related, and share one $3 million limit, if they share a founder or manager, a franchisor, a landlord, a major supplier, an outside investor, or a paid promoter. Family members count as the same person, following the same kind of rules the tax code already uses for family ownership.
|
||||
- **Caps on middlemen.** No single promoter, consultant or referral source can bring in loans totalling more than 3% of the fund.
|
||||
- **Money arrives in stages**, as the business hits milestones, so a sham can't take the full amount and disappear.
|
||||
|
||||
## Founders paying themselves first
|
||||
|
||||
**The trick.** A founder puts in $300,000 as a "loan" and books two years of "unpaid work" at an inflated salary. When the bank's loan arrives, the co-op pays the founder back first, with generous interest. The founder's money is out and safe, and public money has replaced it.
|
||||
|
||||
**The rules** (details in [The Founder Problem](founder-problem.md#the-tools)):
|
||||
|
||||
- **Founders' loans and back pay rank behind the bank.** They can't be repaid from the bank's loan money, or while any payment to the bank is late.
|
||||
- **Back pay is valued at no more than the co-op's own pay for that job** once it has revenue. Founder loans earn at most the Treasury rate plus three points.
|
||||
- **Salaries can't be used to skip the profit split.** Founders can set their own pay in the founding bylaws for at most five years. After that, the members set all salaries. Pay for every role is disclosed to the bank each year, and the gap between the highest and lowest pay is published. Pay bands are open to every worker, nobody votes alone on their own pay, and high pay needs the agreement of the lower-paid half of the members as well as a majority of all of them ([Who Sets the Pay](pay.md)).
|
||||
- **Payouts from members' accounts pass a solvency test**, so insiders who see trouble coming can't cash out first (see [Member Capital Accounts](capital-accounts.md#protecting-the-co-op-and-the-people-who-stay)).
|
||||
|
||||
## Relabeling an old business as "new"
|
||||
|
||||
**The trick.** A failing conventional business re-registers as a co-op to get a loan. That brings back the FONDES rescue problem through the side door. It would also flatter the survival numbers, because the business has already made it through its riskiest early years.
|
||||
|
||||
**The rule.** "New" describes the business, not the paperwork. If a co-op takes over an existing firm's assets, customers, location or most of its staff, it isn't new. It's either refused as a rescue or scored with the existing co-ops.
|
||||
|
||||
## Keeping zombies alive
|
||||
|
||||
**The trick.** If the test fails, the bank shuts down. So the bank's own staff have the strongest reason of anyone to make the numbers look good. They can stretch out loans, let unpaid interest pile up, and keep dead firms technically open. A healthy co-op can also absorb a dying one, so the closure never shows up.
|
||||
|
||||
**The rules:**
|
||||
|
||||
- **"Open" means actually operating:** at least three members and sales above a minimum level. An empty shell counts as closed.
|
||||
- **Loans are valued honestly.** At review, outstanding loans are valued at what a buyer would pay. A loan whose terms were eased to avoid default is marked down. Unpaid interest added to the balance doesn't count as income.
|
||||
- **Mergers are treated the same on both sides.** A co-op absorbed by another co-op, and a conventional firm bought by another firm, are both counted as neither success nor failure. A second analysis counts them both ways.
|
||||
- **A merger that hides a failure counts as a failure.** If a co-op was behind on its loan payments, or below the minimum sales level, at any point in the year before it merged, the merger counts as a closure. Nobody working for the bank, including [the advisers](pilot-spec.md#the-support-lottery), may arrange or suggest a merger.
|
||||
|
||||
## Sabotage
|
||||
|
||||
**The trick.** An opponent recruits 25 or 30 plausible applicants, has them draw down their loans, strip the money and default. That pushes losses past the emergency stop and freezes the pilot before it reaches a verdict.
|
||||
|
||||
**The rules:**
|
||||
|
||||
- **Fraud doesn't end the experiment.** Under the [emergency stop](pilot-spec.md#the-decision-rule), lending restarts once the reviewers confirm the specific gaps are closed and the board signs off. Losses that are later recovered are credited back.
|
||||
- **The staged payouts and middleman caps above** limit how much any ring can take before it's noticed.
|
||||
|
||||
## Rigging the review
|
||||
|
||||
**The trick.** The headline thresholds are fixed, but a lot of smaller choices aren't: which firms count as matches, how closures are detected in the data, which years are included. Lobbyists on both sides would fight over each one, and whoever picks the reviewers picks the answer.
|
||||
|
||||
**The rules:**
|
||||
|
||||
- **The analysis code is filed in advance**, not just the plan, along with a digital fingerprint that shows it hasn't changed.
|
||||
- **The academic reviewers are nominated by the National Academies of Sciences**, and nobody with ties to the bank or to co-op advocacy groups can serve.
|
||||
- **Any change from the filed plan must be signed, explained and published**, and the result under the original plan is always reported next to it.
|
||||
|
||||
## Rigging the price
|
||||
|
||||
**The trick.** A friendly bank chief could quietly undercharge co-ops, which is a hidden subsidy. A hostile one could overcharge them into failure.
|
||||
|
||||
**The rule.** The [annual rate audit](pilot-spec.md#what-market-rate-means) works in both directions. Undercharging is counted as a loss against the bank's results. Overcharging gets the affected loans repriced.
|
||||
|
||||
## Political favoritism
|
||||
|
||||
**The trick.** This is what FONDES actually did: politicians steer loans to allies.
|
||||
|
||||
**The rules:**
|
||||
|
||||
- **Every loan is public:** the borrower, the amount, the rate and the terms.
|
||||
- **Every contact from an elected official or their staff about a specific loan is logged and published.**
|
||||
- **The board answers to elected officials. Individual loans don't.** The President can appoint and remove the bank's board at will. But by law nobody outside the bank can direct a decision on a particular loan except through the board, the board can overrule its staff only in writing and in public ([Who has the last word](durability.md#who-has-the-last-word)), loan officers are career bankers the board hires and can fire only for cause, not political appointees, and a new board can't change the filed lending standards without the results being reported both ways (see [Making It Last](durability.md#what-a-new-board-cant-change)).
|
||||
@@ -0,0 +1,210 @@
|
||||
---
|
||||
title: The Pilot, in Numbers
|
||||
section: Growing the Sector
|
||||
order: 65
|
||||
summary: The concrete pilot: how much money, how many co-ops, what gets measured, against whom, and the exact results that grow the bank or shut it down.
|
||||
---
|
||||
|
||||
# The Pilot, in Numbers
|
||||
|
||||
[The Test](the-test.md) says the bank runs as an experiment. You can't fault an experiment that has no numbers, so this page supplies them. Every number here is a first draft and open to argument. What matters is that they're written down *before* the first loan, so nobody (me included) can pick them after seeing the results.
|
||||
|
||||
The rules that stop people from gaming the pilot (sham co-ops, hidden owners, rigged comparisons) have their own page: [Guarding the Pilot](pilot-safeguards.md).
|
||||
|
||||
## The short version
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **Money** | $350 million, once, plus a separate $100 million for [worker buyouts](buyouts.md). |
|
||||
| **Lending window** | Years 1–5 (up to year 8 if good applicants are scarce). |
|
||||
| **Target** | About 400 new worker co-ops. |
|
||||
| **Loan size** | Up to $3 million per co-op, including all later loans. |
|
||||
| **Price** | Market rate: what a similar conventional business would pay. No personal guarantees from members. |
|
||||
| **Compared against** | Similar new conventional firms that got similar-sized loans, plus a lottery among borderline applicants. |
|
||||
| **Final review** | Year 12, by an independent reviewer. |
|
||||
| **Passes if** | Co-ops stay open about as well as the comparison firms *while paying their members fairly*, and the bank earns at least what the money would have earned in Treasury bonds. |
|
||||
| **Shuts down if** | Either of those fails. |
|
||||
|
||||
The rest of this page explains each line.
|
||||
|
||||
## Two warnings from real life
|
||||
|
||||
Two public efforts to fund co-ops went badly. The pilot is built to avoid repeating them.
|
||||
|
||||
**Uruguay's FONDES.** In 2011 Uruguay set up a public development fund to lend to worker-run firms. By 2016 it had funded 22 firms, and almost 90% of its loans (US$63 million of US$69 million) had been written down as uncollectable. Eight firms got 93% of the money. Three with close ties to the governing party got nearly half. The biggest single loan, US$15 million, went to Alas Uruguay, an airline formed by the workers of the failed state airline. It stopped flying ten months after launch. The original bill capped any one firm at 3% of the fund, but lawmakers raised the cap to 15% before passing it. ([El Observador, 2017](https://www.elobservador.com.uy/nota/-en-que-estan-los-principales-emprendimientos-financiados-por-el-fondes--20176201600); [El Observador, 2015](https://www.elobservador.com.uy/nota/mitad-de-dinero-del-fondes-fue-para-empresas-companeras--201572500))
|
||||
|
||||
What went wrong: most of the money went to *rescuing firms that had already failed* under other owners, a few borrowers got most of it, and politics chose them. Uruguay reformed the fund in 2015. It added a 10% cap per business group and a limit of two loans per firm in five years ([la diaria, 2016](https://ladiaria.com.uy/articulo/2016/6/arrancando-de-cero/)).
|
||||
|
||||
**Venezuela's co-op boom.** After 2001, Venezuela gave co-ops cheap state loans, first pick of government contracts, and tax breaks. Registered co-ops went from under 800 to over 100,000 in a few years. A 2006 government census found only about 50,000 actually operating. Many had been set up to collect the benefits, not to run a business ([YES! Magazine, 2007](https://www.yesmagazine.org/issue/latin-america/2007/05/12/venezuelas-co-op-boom); [Grassroots Economic Organizing](https://geo.coop/blog/some-counter-intuitive-effects-government-support-co-ops-venezuela)).
|
||||
|
||||
What went wrong: cheap money attracts people who want the money, not the co-op. That's the main reason [the bank](the-bank.md) charges market rates.
|
||||
|
||||
## Why a new bank, not existing programs
|
||||
|
||||
Fair question. Congress already made co-ops eligible for the Small Business Administration's (SBA's) main loan program in 2018, through the Main Street Employee Ownership Act. It has barely been used. The SBA kept requiring personal guarantees on those loans, meaning individual members pledge their own homes and savings if the business fails. That doesn't work for a firm owned by dozens of workers ([House Committee report, 2018](https://www.congress.gov/committee-report/115th-congress/house-report/645/1); [NCEO, 2022](https://www.nceo.org/employee-ownership-blog/house-committee-urges-sba-fully-implement-main-street-eo-act)).
|
||||
|
||||
Private co-op lenders exist and do good work, but they're small. Shared Capital Cooperative, lending to co-ops since 1978, had about $20 million in assets in 2021 ([offering statement](https://www.sec.gov/Archives/edgar/data/1722839/000121390021050617/ea148092-1a_sharedcapital.htm)). The pilot's question is what happens when financing on normal commercial terms is available at real scale. The existing channels can't answer that.
|
||||
|
||||
## How much money, and why
|
||||
|
||||
**$350 million, one time.** In the pilot, the bank lends only its own capital. It doesn't borrow on top of it. That makes the worst case easy to state: taxpayers can lose at most $350 million, minus whatever is repaid. A separate $100 million for owners who choose to sell to their workers has its own rules and its own test ([When an Owner Wants to Sell to the Workers](buyouts.md)), which makes the total worst case $450 million.
|
||||
|
||||
It breaks down like this:
|
||||
|
||||
- **About $290 million for loans.** Around 400 co-ops with a typical first loan of about $500,000 is roughly $200 million. The rest is for follow-on loans to co-ops that grow. For comparison, Shared Capital's average loan is about $230,000. The pilot's loans are larger because it's meant to fund real businesses with real payrolls, not only very small ones.
|
||||
- **$30 million, capped, for business help:** $20 million after the first loan (bookkeeping, governance training, legal setup, and more intensive help for half the co-ops, chosen by lottery; see [the support lottery](#the-support-lottery)), and $10 million before it, taken from the loan budget, for a lottery testing whether serious help makes more groups succeed in starting a co-op that lasts ([Help Before the First Loan](before-launch.md)). Venezuela's census blamed many of its failures on the lack of exactly this. This money is a public cost, stated openly and capped, and it's counted in the pilot's total cost to taxpayers. It isn't allowed to hide inside loan pricing.
|
||||
- **The remainder** is a cash cushion and running costs. Running costs include audits of small co-ops: about 20 a year drawn by lot, plus those triggered by warning signs, perhaps 30 to 50 in all. At $10,000 to $30,000 each, in line with the SEC's estimates for small firms, that's roughly $300,000 to $1.5 million a year. Like any lender's checking, it's a cost of lending, paid from the bank's income and built into its market rate. Audits of buyout co-ops are charged to the buyout track ([What the Rules Cost a Small Co-op](compliance.md#audits-by-lot-and-when-something-looks-wrong)).
|
||||
|
||||
## Why about 400 co-ops
|
||||
|
||||
This is the number that lets the test tell a real answer apart from luck.
|
||||
|
||||
New businesses close a lot. Of all US business locations opened in 2013, only about 35% were still open ten years later ([Bureau of Labor Statistics](https://www.bls.gov/bdm/us_age_naics_00_table7.txt)). With closures that common, a small pilot is noise: 30 co-ops could look great or terrible by chance.
|
||||
|
||||
To be confident that co-ops do "at least about as well" as conventional firms, you need to see several hundred closures across both groups. Standard survival statistics say that about 400 co-ops, each compared with five similar conventional firms, give roughly a 90% chance of a clear verdict if co-ops really are exactly as durable. If they're *more* durable, as the best existing study suggests (below), a clear verdict is easier to reach.
|
||||
|
||||
That also means a roughly 10% chance of shutting down a pilot that was actually working. That's the cost of putting the burden of proof on the co-ops, and it's stated openly.
|
||||
|
||||
**The honest problem with 400.** The whole US worker co-op sector was about 751 firms in 2023, roughly triple what it was a decade earlier ([NCEO summary of the Democracy at Work Institute census](https://www.nceo.org/employee-ownership-blog/democracy-at-work-institute-releases-its-2023-worker-cooperative-state-of-the-sector-report)). That's net growth of about 50 co-ops a year, after counting the ones that closed. Reaching 400 in five years means about 80 a year, roughly 1.6 times that pace, on top of whatever else the sector adds.
|
||||
|
||||
That creates exactly the pressure that sank Venezuela: an institution with a target lowers its standards to hit it. So:
|
||||
|
||||
- **No lending targets.** Nobody is paid, promoted or judged on loan volume. Part of the loan money is set aside for co-ops in lower-paid work, but that's money made available, not a target ([Who the Bank Lends To](who-gets-loans.md)).
|
||||
- **If good applications fall short, the lending window stretches** (up to year 8) instead of standards dropping. The final review moves back by the same amount. That rule is fixed now.
|
||||
- **A shortfall is itself a result.** If good co-op applicants don't show up even when financing is available, that's evidence for the "co-ops are rare because they're hard, not because they're unfunded" explanation, and it gets reported that way.
|
||||
|
||||
## Who can borrow
|
||||
|
||||
- **New worker co-ops only, for the main test.** "New" means the *business* is new, not just the legal paperwork. A conventional firm that reincorporates as a co-op isn't new (see [Guarding the Pilot](pilot-safeguards.md)).
|
||||
- **Existing co-ops may borrow too, but they're scored separately.** Firms that have already survived for years would make the results look better than they are.
|
||||
- **No rescues of failed conventional firms.** That's where FONDES lost its money.
|
||||
- **Healthy firms whose owners choose to sell to their workers** can borrow on a separate track, with its own money and its own test, so they can't flatter the main result ([When an Owner Wants to Sell to the Workers](buyouts.md)).
|
||||
- **At least three worker-members at the first loan**, meeting the [co-op definition](worker-ownership.md) and accepting [the lock](the-lock.md).
|
||||
- **Members are on the payroll as employees.** That gives them minimum wage and workers' compensation like any other worker, and unemployment insurance in most states ([Owners Are Still Workers](labor-law.md)). It also means the co-op shows up in the government business records the test relies on.
|
||||
- **Any legal industry.** Loan officers judge business plans, not industries. But no single industry may take more than 25% of the money lent, and no single county more than 10%, so one bad sector or one local market can't sink the whole test.
|
||||
|
||||
## Loan terms
|
||||
|
||||
- **$3 million per co-op, lifetime.** That's under 1% of the fund. FONDES let one airline take over a fifth of its money. That can't happen here. Related co-ops share one limit (see [Guarding the Pilot](pilot-safeguards.md)).
|
||||
- **Paid out in stages** as the business hits agreed milestones, not in one lump.
|
||||
- **No personal guarantees from members.** The risk that the business fails is exactly the risk the bank exists to price and carry. Loans can be secured by the business's own assets, like any business loan.
|
||||
- **Underwriting can rely on cash flow**, not only on collateral. Collateral-heavy lending mostly measures how much wealth the founders already had, which shuts out the workers co-ops tend to serve.
|
||||
|
||||
## What "market rate" means
|
||||
|
||||
The bank prices each loan the way a normal lender would price a conventional business with the same numbers: same revenue, same industry, same track record, same collateral. Because there are no personal guarantees, the rate is higher than it would be with them. That's the honest price of the risk.
|
||||
|
||||
Two checks keep the price honest:
|
||||
|
||||
- **An annual audit**, in both directions. Each year an outside auditor compares a sample of the bank's loans with rates on comparable conventional small-business loans. It also checks the things that don't show up in the rate: who gets approved, how much collateral is required, and how often troubled loans get softer terms. If loans are systematically too cheap, the shortfall is counted as a loss against the bank's results. If they're too expensive, the affected loans are repriced.
|
||||
- **A private market check.** Every loan is offered to private and community lenders, who can buy up to 20% of it on the same terms. How often they say yes is published. If nobody ever wants a piece, that suggests the price is too low for the risk.
|
||||
|
||||
## What gets measured
|
||||
|
||||
All of this is written down and filed in a public study registry before the first loan, the way serious medical trials and a few careful social-policy studies already do. One example is the [Oregon Health Insurance Experiment](https://www.nber.org/programs-projects/projects-and-centers/oregon-health-insurance-experiment/oregon-health-insurance-experiment-background). The actual analysis code is filed too, so the method can't quietly change later.
|
||||
|
||||
**The two measures that decide the outcome:**
|
||||
|
||||
1. **Fair survival.** Does the co-op stay open and keep treating its members fairly? A co-op counts as *failed* if any of these happens:
|
||||
- it closes, or falls below three members or a minimum level of sales;
|
||||
- for two years running, its members' median pay per hour (wages plus profit share) falls below 85% of the matched conventional firms' median;
|
||||
- it stops being a real co-op: from its third year, fewer than three-quarters of its work hours are done by members, or its workers are kept on probation past the limit, or it pushes people out before they'd become members ([When People Don't Stay Long](high-turnover.md)).
|
||||
|
||||
The pay rule matters. Co-ops can survive a bad stretch by cutting their own pay instead of cutting jobs, and the best existing study couldn't rule that out. A co-op that survives by paying its people far less than the business down the street hasn't passed.
|
||||
2. **The bank's money.** At the end, after every loss, write-off and running cost, and every dollar spent on business help, has the loan book earned at least what the same money would have earned in Treasury bonds over the same years? Anything less means taxpayers lost money compared with just holding bonds. Loans still outstanding are valued at what a buyer would pay for them. Loans that were restructured to avoid default are marked down, so going easy on struggling borrowers can't hide losses.
|
||||
|
||||
A useful yardstick: among the SBA's mature loan groups from 1992 to 2004, loans making up about 6% to 10% of the money lent eventually defaulted, and about 37% of the defaulted amount was later recovered ([GAO-20-618](https://www.gao.gov/products/gao-20-618)). A lender charging market rates with losses in that range covers them from interest.
|
||||
|
||||
**Measures that are published but don't decide:** jobs created (with growth compared with matched firms, spin-offs, and expansions members voted down; see [When Growing Means Sharing](growth.md#5-measuring-it)), wages at the bottom and top and the gap between them, the share of pay above the going rate, how often higher-paid members leave, pay gaps by gender and race within the same job ([Who Sets the Pay](pay.md)), revenue per worker (a rough measure of productivity) and value added per hour worked, followed as co-ops grow ([Does Sharing the Profit Mean Less Effort?](free-riding.md#measuring-it)), how long members stay, injury rates, and the bank's default and recovery rates. Also:
|
||||
|
||||
- **Who got loans.** Approval rates and loan terms by the founders' race, gender and personal wealth. If collateral-based lending quietly shuts out the people co-ops mostly serve, it should show. Results are also reported for co-ops in lower-paid work and for first-time owners without much wealth ([Who the Bank Lends To](who-gets-loans.md)).
|
||||
- **Who gets the profits inside each co-op.** Profit shares, founding shares and membership approvals by race and gender, so that an in-group of early members can't quietly take most of the pool.
|
||||
- **Who founded them.** Founders' previous earnings and work histories, using linked government records where they're available, compared with the founders of the matched firms. If the strongest founders stay away ([The Founder Problem](founder-problem.md)), this is where it shows. How it's measured, and how the verdict has to say how much founders explain, is in [Co-ops or Their Founders?](who-founds.md)
|
||||
- **Investment.** Spending on equipment, buildings and development per worker, how much of each year's profit is kept in the business, and the return those investments earn, compared with the matched firms. If co-ops starve themselves of capital, as economists have long predicted ([Investing for the Long Run](the-long-run.md)), this is where it shows.
|
||||
- **Local effects.** Business openings and closures in the counties where co-ops were funded, so any damage to existing local businesses is visible.
|
||||
- **Results by industry**, including care work like home health and cleaning, where most US co-op members work today.
|
||||
|
||||
Why only two deciding measures: the more measures you allow to count, the easier it is to find *one* that looks good. Two, fixed in advance, can't be gamed that way.
|
||||
|
||||
## Who co-ops are compared against
|
||||
|
||||
This is the hardest part of any test like this. Co-ops that got a loan aren't like the average new business. They had a plan good enough to get a loan. Comparing them with *all* new businesses would flatter them.
|
||||
|
||||
**Main comparison: new conventional firms that got a similar-sized loan through the SBA's main program.** They're matched on industry, region, start year, starting size, legal form, and the number of founders. Businesses with more founders tend to survive longer, and co-ops always have at least three. This group is imperfect in both directions. SBA borrowers had to show they couldn't get credit elsewhere, which makes them a weaker comparison. But their loans carry a government guarantee that makes credit cheaper for them, which makes them a stronger one. The reviewer reports both biases openly. Researchers have already matched SBA borrowers with similar firms in government business records ([Brown and Earle, *Journal of Finance*, 2017](https://papers.ssrn.com/abstract=2641184)), so the data work is feasible.
|
||||
|
||||
**Second check: a lottery at the margin.** Among applicants whose scores land just around the approval line, the bank decides by lottery who gets a loan. Then it follows both groups. This is the cleanest test of the core claim: *does access to financing make co-ops that would otherwise not exist, or not survive?* The lottery group will be small, so it supports the main result rather than replacing it.
|
||||
|
||||
**Third check:** co-ops against *all* similar new firms.
|
||||
|
||||
**Sales count the same way on both sides.** A conventional firm that gets bought hasn't necessarily failed, and a co-op can't be bought at all because of [the lock](the-lock.md). So acquisitions and mergers, on either side, are counted as neither success nor failure in the main analysis, with a second analysis counting them both ways.
|
||||
|
||||
**What existing evidence predicts.** The strongest study so far followed every firm in Uruguay from 1997 to 2009: about 29,000 firms, 223 of them worker-run. After leaving out very small businesses and adjusting for tax differences, worker-run firms were about 29% *less* likely to close in any given period than conventional ones ([Burdín, *ILR Review*, 2014](https://journals.sagepub.com/doi/10.1177/001979391406700108)). The advantage was clearest in services. In manufacturing and transport there was no clear difference. That's encouraging, but it's one country, and it's why pay is built into the survival measure above.
|
||||
|
||||
## The support lottery
|
||||
|
||||
Mondragon's bank gave its co-ops far more than loans. It gave them a salaried manager while they planned, specialist departments for marketing and production, a staff member who sat in on board meetings for the first year, and monthly check-ins on the numbers ([Has This Been Tried?](precedents.md#mondragons-bank-caja-laboral)). Nobody knows whether the loans or the help did the work. The pilot can test part of that cheaply, so it does.
|
||||
|
||||
**How it works:**
|
||||
|
||||
- **Every co-op gets standard help:** setting up the books, legal setup, and training in how to run a co-op meeting and a co-op budget. That's about $15,000 per co-op, around $6 million in all.
|
||||
- **Half also get intensive help for two years, chosen by lottery.** That means an experienced adviser who attends board meetings without a vote, a monthly review of the numbers, and access to specialists in marketing, operations and pricing. It costs about $70,000 more per co-op, around $14 million in all.
|
||||
- **The draw is made by the reviewers, not the bank**, after each co-op's first loan payment, using a random method filed in advance. It's balanced by industry and by the founders' wealth. Every co-op is analyzed in the group it was drawn into, whether or not it uses the help, so a co-op can't improve the numbers by opting in or out.
|
||||
- **Each co-op's help budget is set aside when it's drawn.** If money runs short, the intensive group gets smaller, but no co-op's help gets thinner.
|
||||
- **Co-ops agree to the lottery when they apply**, and the design is approved by an independent research ethics board. Co-ops in the standard group can ask for intensive help after year four, once the main comparison has been measured.
|
||||
|
||||
**Advisers are walled off from the bank's interests.**
|
||||
|
||||
- **They're hired through outside nonprofit providers on fixed fees.** They aren't bank staff, and nothing they're paid depends on whether their co-ops survive.
|
||||
- **They advise; they don't run anything.** All advice is written and non-binding. They can't vote, sign anything, or take part in hiring or firing, and the members can replace their adviser at any time.
|
||||
- **They can't arrange rescues.** Advisers can't arrange or suggest mergers, loan changes or sales of the business. A co-op's merger counts as a closure if it was behind on payments or below the minimum sales level in the year before (see [Keeping zombies alive](pilot-safeguards.md#keeping-zombies-alive)). Merger and restructuring counts are published for each group.
|
||||
- **They're treated as insiders.** An adviser and their firm can't sell paid services to a co-op they advised for three years afterward. Someone who referred a co-op for a loan can't be its adviser.
|
||||
- **Their one reporting duty is fraud.** They report suspected fraud to the reviewers, and nothing about business performance goes to the lending side.
|
||||
|
||||
**The help is a subsidy, and it's counted as one.** Intensive help is worth something like a sixth of a typical first loan. So:
|
||||
|
||||
- **Every dollar of business help counts against the bank** in the [money test](#the-decision-rule), just like a bad loan.
|
||||
- **The survival result for the standard-help group alone is published next to the main result.** If co-ops only hold their own with intensive help, readers will see that.
|
||||
- **Comparison firms' use of free government business counseling**, such as the SBA's Small Business Development Centers, is recorded where the data allows.
|
||||
|
||||
**What it can and can't show.** Assume about 200 co-ops in each group and closures in line with new businesses generally. Then the pilot can reliably detect only a large effect, something like 30 to 40% fewer closures in the intensive group. So its main outcome is survival and fair pay at year four, when the help's effect should be clearest, with year twelve reported as well. The result is reported as a range, with a cost per closure avoided, and it counts as exploratory. **A result that isn't statistically significant doesn't prove the help is useless.** It means only that a large effect has been ruled out.
|
||||
|
||||
**What it doesn't test.** Mondragon's help started *before* a co-op existed: studying the idea, paying the manager, lending to cover early losses. This lottery starts after the loan, so a second lottery, among groups that want to start a co-op, tests help before launch ([Help Before the First Loan](before-launch.md)).
|
||||
|
||||
**If intensive help works,** that's an argument for building it into [the federation](federation.md), paid for by the co-ops out of their shared fund the way Italy's Coopfond is, not for a permanent federal advisory office.
|
||||
|
||||
## The timeline
|
||||
|
||||
- **Year 0.** Charter the bank. File the full analysis plan, code, benchmarks and thresholds. Name the reviewers.
|
||||
- **Years 1–5.** Lending window, stretching to year 8 if applications fall short.
|
||||
- **Year 6.** Public interim report. It can't trigger growth. It can only trigger the emergency stop.
|
||||
- **Year 12.** Final review. Every co-op funded has been open for at least seven years. That covers most of the danger period for new businesses and, almost certainly, at least one recession.
|
||||
|
||||
**The reviewers:** the Government Accountability Office (GAO), Congress's own auditor, which already reviews SBA lending, working with academic economists nominated by the National Academies of Sciences. They must have no ties to the bank or to co-op advocacy groups. The bank's staff don't grade their own work. The reviewers can depart from the filed plan only in a signed, published explanation, and the filed plan's result is always reported alongside.
|
||||
|
||||
## The decision rule
|
||||
|
||||
At year 12, both measures must pass:
|
||||
|
||||
1. **Fair survival.** We can be 95% confident that co-ops' chance of failing in a given year, as defined above, is no more than 25% higher than the comparison group's. If co-ops are exactly as durable or more, this should pass. If they're clearly worse, it fails.
|
||||
|
||||
Why allow up to 25% rather than demanding exact equality? No test can prove two things are *exactly* equal. You can only rule out differences bigger than some margin, and a tighter margin takes more co-ops. Ruling out a 10% gap would take somewhere around 1,500 to 2,000 co-ops, more than twice the entire US co-op sector. The 25% margin is also a value judgment, stated openly: a society might reasonably accept firms that are a little less durable in exchange for workplaces people govern themselves. Readers who think that price is too high should say so. Burdín's result suggests the margin won't matter, because co-ops may well come out ahead.
|
||||
2. **The bank's money** earned at least the Treasury rate, counting every loss, every running cost, and every dollar of business help.
|
||||
|
||||
**If both pass:** the charter triggers a fast-track, up-or-down vote in Congress on a second capitalization, with no amendments and no committee bottleneck. It can't be automatic, because no Congress can bind a later one's spending. A fast-track vote is the most that can honestly be promised. If the bank is later allowed to borrow, its debts carry **no federal guarantee**, written into the charter. Fannie Mae and Freddie Mac needed a taxpayer rescue in 2008 partly because markets had always assumed the government stood behind them. This bank shouldn't be able to create that assumption.
|
||||
|
||||
**If either fails, the shutdown happens by itself, with no further vote needed.** The board formally adopts the reviewers' result, and after a fail it has to start the loan sale by a deadline the law sets ([Who has the last word](durability.md#who-has-the-last-word)). No new loans. The remaining loans are offered at fair value to co-op and community lenders, who keep the same repayment terms. Whatever they don't buy is collected by the Treasury on the original terms until paid off. None of it goes to distressed-debt buyers. The money goes back to the Treasury. The co-ops stay co-ops, because [the lock](the-lock.md) is in their own charters, but they're on their own.
|
||||
|
||||
**There is no "inconclusive, keep going" option.** If the evidence doesn't clearly show co-ops holding their own, that counts as a fail. The burden of proof is on the co-ops, which is where a skeptic would put it.
|
||||
|
||||
**The emergency stop.** If net losses ever pass 20% of the capital ($70 million), new lending freezes (the [buyout track](buyouts.md) has its own stop, at $20 million), and the reviewers report within six months on the cause:
|
||||
|
||||
- **A recession:** lending restarts only if the SBA's own loans from the same years lost money at a similar rate. That's a fixed comparison, not a judgment call.
|
||||
- **Fraud:** lending restarts once the reviewers certify the specific gaps are closed and recovery has been pursued, and the board signs off in writing. Otherwise one organized fraud ring could end the experiment on purpose (see [Guarding the Pilot](pilot-safeguards.md)).
|
||||
- **Bad lending decisions:** no restart. That's a result.
|
||||
|
||||
## What this pilot can't tell you
|
||||
|
||||
- **Whether the labor market changes.** 400 co-ops won't move anyone's wages. How big the sector would need to get, how long that could take, and how the effect could be tested locally after a pass are in [After a Pass: How Big, How Fast](scale.md).
|
||||
- **Whether the best founders come.** The test measures the co-ops that *did* apply. If strong founders stay away ([The Founder Problem](founder-problem.md)), a pass means "the co-ops that exist can compete," not "co-ops would win with everyone's best people." The pilot can measure how much founders' backgrounds explain the result, but it can't see the founders who never applied ([Co-ops or Their Founders?](who-founds.md)).
|
||||
- **Whether co-ops do better with a full support network.** The pilot funds individual co-ops. It doesn't build [the federation](federation.md) that Mondragon relied on, so co-ops that join a federation are reported separately. The [support lottery](#the-support-lottery) tests one piece of that network, hands-on help, but not the rest: shared funds, moving workers between co-ops, joint buying and selling.
|
||||
- **Whether it survives politics.** Congress can refuse the second capitalization, or repeal the whole thing. A hostile board could also try to rig the result, which is why the verdict can be "not tested" rather than "failed" if the bank turned away applicants who met its own standards. See [Making It Last](durability.md).
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: Has This Been Tried?
|
||||
section: Growing the Sector
|
||||
order: 55
|
||||
summary: Three real institutions that financed co-ops (Mondragon's bank, America's co-op bank, and Italy's co-op funds), what they got right, how they went wrong, and what the pilot takes from each.
|
||||
---
|
||||
|
||||
# Has This Been Tried?
|
||||
|
||||
A bank that lends to co-ops isn't a new idea. At least three serious versions exist, and each teaches something different. One built a whole economy of worker co-ops, and then quietly became an ordinary bank. One was created by Congress, and ended up lending to expensive New York apartment buildings. One is funded by co-ops themselves and still holds most of what it has collected over thirty years.
|
||||
|
||||
This page goes through what each one actually did, with the failures left in, and what [the bank](the-bank.md) and [the pilot](pilot-spec.md) take from them.
|
||||
|
||||
## Mondragon's bank: Caja Laboral
|
||||
|
||||
**What it was.** In 1959 the young Mondragon co-ops in Spain's Basque Country founded their own savings bank, the Caja Laboral Popular ("the Working People's Bank"). Local people deposited their savings there, and every member co-op was required to bank there too. Until the 1980s it lent almost only to co-ops. Apart from some government job-creation money, it was their only source of capital from outside their own members, and its loans made up about half of the co-ops' capital ([Morris, *The Mondragón System*, 1992](https://www.ilsr.org/wp-content/uploads/2013/03/mondragon-3.pdf)).
|
||||
|
||||
**What made it unusual wasn't the money. It was the help.** In 1969 the bank set up a separate Entrepreneurial Division, which worked more like a business incubator than a loan desk (Morris, 1992):
|
||||
|
||||
- When a group of workers came with an idea, the bank picked one of them to be manager, **paid them a salary**, and put them on a team that studied whether the business could work. That often meant visiting factories elsewhere in Europe to learn how the best ones did it.
|
||||
- The bank had departments for marketing, exports, production planning, factory design, legal setup and accounting, and the new co-op could use all of them.
|
||||
- The bank put up 60% of the start-up money. The government's job-creation program put up 20%, and the founders themselves 20%. The bank also covered the new co-op's operating losses for its first two years, as a loan to be repaid later.
|
||||
- One bank staff member was assigned to each new co-op as its "godfather" and sat in on its board meetings for the first year.
|
||||
- Every co-op sent the bank monthly financial reports and got a full audit every four years.
|
||||
|
||||
**The famous number, and why it needs care.** Of the 103 co-ops created between 1956 and 1986, only three were shut down ([Whyte and Whyte, *Making Mondragon*, 1988, as summarized by Participedia](https://participedia.net/case/82)). That counts co-ops the bank rescued, restructured or merged as survivors, and five things sit behind it:
|
||||
|
||||
- **The money was cheap.** One new co-op, a maker of seed drills, borrowed at 8% when the going market rate was 15%. The bank could do this partly because every co-op had to do its banking there, which kept its cost of money low, and partly because its managers were paid co-op wages (Morris, 1992).
|
||||
- **The help was subsidized.** The economist David Ellerman estimated in the early 1980s that what new co-ops repaid for their start-up support covered less than half of what it actually cost the bank (Morris, 1992).
|
||||
- **Survival often meant rescue.** In Spain's recession of the late 1970s and early 1980s, the bank's business division switched from starting co-ops to saving them. In 1983 alone, 34 of about 100 co-ops needed its intervention department. The bank replaced managers, changed product lines, merged co-ops, and moved workers between them, and it wrote off about $160 million in loans (Morris, 1992). Many of the co-ops that "survived" did so because the bank restructured them. That's a real achievement, but it isn't the same as surviving on their own.
|
||||
- **The market was protected.** Mondragon's biggest industry was household appliances. In 1980 Spain protected them from imports with a 35% tariff, and that protection only fell away (to 5% in 1991) as Spain joined the European market (Morris, 1992).
|
||||
- **Then the bank moved on.** Once a change in Basque co-op law allowed it, in the early 1980s, the bank started offering mortgages and car loans to the public, and its deposits grew far beyond what the co-ops needed. By the early 1990s only about 15% of its loans went to co-ops. In 1991 the Entrepreneurial Division was spun off into a separate consulting co-op, LKS (Morris, 1992).
|
||||
- **And the flagship later failed.** Fagor, the appliance maker at the heart of the group, expanded across Europe and China largely on borrowed money, lost its main market in Spain's housing crash, and went bankrupt in 2013 with about €800 million (about $1.1 billion) of debt ([The Local, 2013](https://www.thelocal.es/20131114/major-manufacturer-fagor-files-for-bankruptcy)). Full group support didn't save it.
|
||||
|
||||
**What the pilot takes from it:**
|
||||
|
||||
1. **Maybe the help mattered more than the money.** Mondragon combined cheap loans, a protected home market and intensive hands-on support, and nobody can tell which one did the work. The pilot tests one piece of that directly (see [the support lottery](pilot-spec.md#the-support-lottery)).
|
||||
2. **Count rescues honestly.** A bank that can quietly restructure its borrowers can make almost any survival rate look good. That's why the pilot counts a co-op that's been restructured to avoid default as a loss, not a survivor (see [Keeping zombies alive](pilot-safeguards.md#keeping-zombies-alive)).
|
||||
3. **A co-op bank drifts toward ordinary lending once co-op demand runs out.** The pilot's bank can lend only to worker co-ops, and the bank can't change its own charter (see [The Bank](the-bank.md)). That doesn't stop a legislature from changing it, which is how Caja Laboral's drift began.
|
||||
4. **The pilot doesn't copy the cheap money.** It lends at market rates, for the reasons on [the bank's page](the-bank.md). The help it gives *is* a subsidy, so it's counted against the bank's results and tested separately (see [the support lottery](pilot-spec.md#the-support-lottery)).
|
||||
|
||||
## America's co-op bank: the National Consumer Cooperative Bank
|
||||
|
||||
**What it was.** In 1978 Congress created the National Consumer Cooperative Bank to lend to co-ops that couldn't get credit from ordinary banks. It came with a separate office to give new co-ops technical help and start-up money. The law authorized up to $300 million of federal capital over five years ([Public Law 95-351](https://www.congress.gov/95/statute/STATUTE-92/STATUTE-92-Pg499.pdf)). According to one of its later board members, the public investment that actually went in was about $100 million, to be repaid over 40 years ([Barnes, board member 1982–86](https://peter-barnes.org/blog/what-ever-happened-to-the-national-cooperative-bank/)).
|
||||
|
||||
**Why it never became a worker-co-op lender.** The law was written for co-ops whose members are *consumers*: food co-ops, housing co-ops, co-ops that buy things together for their members. To qualify, a co-op has to serve members "who are ultimate consumers" of what it provides. Co-ops of *producers* can borrow, but loans to them are capped at 10% of the bank's assets ([12 U.S.C. § 3015](https://www.law.cornell.edu/uscode/text/12/3015)). A worker co-op, whose members supply their work rather than buy its products, fits that law awkwardly at best. Worker co-ops were never what it was built for.
|
||||
|
||||
**What happened to it.** The Reagan administration tried to shut it down in 1981. Congress resisted, and the compromise was to privatize it. On December 31, 1981 it became a co-op owned by its borrowers, and by 1985 it was fully private and renamed the National Cooperative Bank ([Next City, 2019](https://nextcity.org/urbanist-news/the-weirdest-bank-youve-never-heard-of); [Encyclopedia.com](https://www.encyclopedia.com/law/encyclopedias-almanacs-transcripts-and-maps/national-cooperative-bank)). It survived, and it had about $2.6 billion in assets in 2019. Its technical-help office was later spun off as a separate nonprofit ([12 U.S.C. § 3051](https://www.law.cornell.edu/uscode/text/12/3051)). But in the words of one of its former directors, it became "a big lender to fancy New York City housing coops, large grocery wholesalers" and other "healthy businesses, few of whom need help getting credit" (Barnes).
|
||||
|
||||
**What the pilot takes from it:**
|
||||
|
||||
1. **The charter decides who gets served.** A bank "for co-ops" will lend to whichever co-ops are safest. The pilot's bank is chartered for worker co-ops only, and only those that accept [the lock](the-lock.md).
|
||||
2. **A target nobody enforces is not a target.** The 1978 law told the board to use its "best efforts" to keep at least 35% of its loans going to co-ops of low-income people or facilities they use (Public Law 95-351). Missing that target had no consequence, and the bank drifted toward borrowers that could have borrowed anywhere. The pilot has fixed measures, published in advance, and a date when it has to answer for them (see [The Test](the-test.md)).
|
||||
3. **Politics can change what a bank is.** It survived an attempt to close it by becoming something else. A charter can't protect against the legislature that wrote it. The pilot is built so that a failed test shuts it down cleanly, rather than letting it drift into a different mission (see [The Constitution](constitution.md)).
|
||||
|
||||
## Italy's co-op funds: Coopfond and the Marcora Law
|
||||
|
||||
Italy has two different kinds of co-op fund, and both are useful.
|
||||
|
||||
**Coopfond: money from the co-ops themselves.** Since 1992, Italian law has required every co-op to pay 3% of its annual profit into a co-op development fund, usually the one run by its federation ([Lomuscio and Tortia, 2023](https://pdfs.semanticscholar.org/a7eb/90b586cb2adb63c321f1ca616afefb27eb70.pdf)). Coopfond is the fund of Legacoop, the largest federation. It also receives whatever is left of a co-op's assets when it's wound up, or when it converts into an ordinary company. It invests in co-ops through loans and temporary, time-limited stakes, and recycles the money as it's repaid. From 1993 to 2023 it collected about €596 million. After three decades of losses and grants, about €499 million, or 91% of what it collected, is still in the fund. Around 2,400 co-ops pay in, about €18–20 million a year ([Coopfond, 2022–23 sustainability report](https://www.coopfond.it/wp-content/uploads/bilancio24_compressed.pdf)).
|
||||
|
||||
Two cautions. Those are euros as collected, not adjusted for thirty years of inflation, so the fund's real value has shrunk more than the 91% suggests. And it isn't a one-time endowment: new money arrives every year, and Italian co-ops pay the 3% alongside generous tax breaks on their locked reserves. Still, it's the closest real-world model for the [federation's](federation.md) shared fund: the movement financing its own growth without spending its capital away.
|
||||
|
||||
**The Marcora Law: public money matched to workers' own.** Under a 1985 law named after the industry minister who proposed it, Giovanni Marcora, the Italian state helps workers buy out firms that are closing. It invests alongside the workers through CFI, a specialist co-op investor whose members include the industry ministry, Italy's business development agency Invitalia, and about 270 co-ops. Before investing, CFI studies whether the business can work and advises the workers, much like Mondragon's Entrepreneurial Division. Workers put in their own money too, often from their severance and unemployment benefits ([Vieta, Euricse working paper, 2015](https://base.socioeco.org/docs/wp-78_15_vieta.pdf)).
|
||||
|
||||
The survival record is good. Of the 82 buyouts started between 2003 and 2014, 71 (87%) were still operating at the end of 2014 (Vieta, 2015). That's a young group, though, and buyouts are chosen with care: workers only try one when they're confident the business can work.
|
||||
|
||||
These are rescues of existing businesses, which [the pilot](pilot-spec.md#who-can-borrow) keeps out entirely because of Uruguay's experience. It does lend for sales of *healthy* firms whose owners choose to sell to their workers, on a separate track ([When an Owner Wants to Sell to the Workers](buyouts.md)). The difference is in how they're chosen. Marcora buyouts need the workers' own money and a specialist's study of whether the business can work. Uruguay's went mostly to politically connected firms that had already failed.
|
||||
|
||||
**The warning in the Marcora story.** The original law let the state put in up to three euros for every euro the workers invested. After Italy's main employers' association intervened, the European Union ruled that this broke its competition rules by giving worker buyouts an unfair advantage, and the program was suspended. A 2001 reform cut the state's share to one euro per worker euro, to be repaid over 7 to 10 years. From 2000 to 2006, while the rules were being rewritten, CFI made no new investments, and only a handful of buyouts happened at all (Vieta, 2015).
|
||||
|
||||
**What the pilot takes from it:**
|
||||
|
||||
1. **A movement fund can last.** Coopfond shows that a small cut of co-op profits, lent and invested carefully, can keep its value for decades. The federation fund is modeled on it.
|
||||
2. **Workers' own money in, alongside the public's.** Both Mondragon (20% from founders) and Marcora (one to one) required workers to put money in. The pilot now asks founders for a small stake, at least 5% of the first loan, which they can pay in unpaid work instead of cash, because a cash buy-in shuts out workers with no savings ([Help Before the First Loan](before-launch.md)).
|
||||
3. **Subsidies get challenged.** Marcora's generous matching was the part that got struck down, and the program lost six years. The US has no European-style rule against state aid, but the political version of the same attack ("unfair competition with taxpayer money") works anywhere. A bank that lends at market rates is much harder to attack that way. That's one more reason the pilot's bank doesn't subsidize its loans, and why the help it does give is costed openly.
|
||||
4. **Italy locks the reserves, not the firm.** An Italian co-op *can* turn itself into an ordinary company, but its accumulated reserves go to the co-op development fund, not to the members. That's a softer version of [the lock](the-lock.md), and it's weighed in [Open Problems](open-problems.md).
|
||||
|
||||
## The pattern
|
||||
|
||||
| | Mondragon's bank | US co-op bank | Coopfond | Marcora / CFI |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **Money came from** | Local savers and the co-ops' own deposits | Federal capital | 3% of co-op profits | The state, matched to workers' money |
|
||||
| **Hands-on help** | Very intensive | A separate office, later spun off | Some | Feasibility study and advice |
|
||||
| **Who it served** | Worker co-ops | Mostly housing and consumer co-ops | Co-ops of all kinds | Worker buyouts |
|
||||
| **What went wrong** | Rescues flattered its record; it drifted into ordinary banking; its flagship failed | Drifted to safe borrowers after privatization | Its real value shrank with inflation | Its subsidy was ruled unfair and the program suspended |
|
||||
|
||||
Four cases can't prove a rule. But two patterns stand out. The two that actually built worker co-ops, Mondragon and Marcora, both paired money with serious hands-on help before and after launch. And both banks drifted once their mission became optional, one because the law changed and one because nobody enforced its target. The pilot takes both lessons: it tests whether the help matters, and its mission comes with a deadline and a verdict.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Prison Labor
|
||||
section: Beyond Co-ops
|
||||
order: 120
|
||||
summary: Only with full pay parity, and only if no budget ever benefits from more people being locked up.
|
||||
---
|
||||
|
||||
# Prison Labor
|
||||
|
||||
Incarcerated people work **the same jobs, at the same pay, side by side with free temps.**
|
||||
|
||||
Pay parity is not a detail. It is the entire moral load-bearing wall, and it is the first thing a critic will assume was left out, so it goes in the first sentence, always.
|
||||
|
||||
Pay parity is what separates this from convict leasing. The historical horror ran on a *cost advantage*: after the Civil War, Southern prisons became labor suppliers and arrests rose to meet demand. The Thirteenth Amendment abolished slavery *"except as a punishment for crime,"* and that clause is why it worked, and it is still in the Constitution. Remove the cost advantage and the state gains nothing by locking more people up.
|
||||
|
||||
**The test, applied forever:** *if automation arrived tomorrow and every prison job vanished, would any budget line get harder to balance?* If yes, the trap is live and the design has failed.
|
||||
|
||||
Required guardrails: pay parity, genuinely optional participation, no quotas, no budget line that improves with a larger prison population.
|
||||
|
||||
**The goal is reform, not punishment.** People should leave prison with savings, a work history, and a reference. Employment after release is one of the strongest predictors of not going back.
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
title: Protecting Members' Savings
|
||||
section: The Co-op
|
||||
order: 26
|
||||
summary: If a co-op fails, its workers shouldn't lose their last paychecks and their savings along with their jobs. How an insurance pool paid for by the co-ops protects them, and what it can't promise.
|
||||
---
|
||||
|
||||
# Protecting Members' Savings
|
||||
|
||||
Every member of a co-op builds up savings in a [capital account](capital-accounts.md). That money is real, but it sits inside the same business that pays the member's wages. If the co-op fails, its lenders get paid first, and members' accounts are paid only from whatever is left. A worker can lose their job and their savings on the same day. Their last few paychecks may go unpaid too.
|
||||
|
||||
The model already limits how much can pile up (accounts are capped at a year's pay) and stops insiders cashing out first (every payout has to pass a solvency test). This page adds the missing piece: insurance.
|
||||
|
||||
## What goes wrong in real life
|
||||
|
||||
- **Fagor, 2013.** When Mondragon's appliance maker went bankrupt, more than 900 former workers of Fagor and a related co-op sued Mondragon for €47.8 million of capital contributions they had lost. The courts turned them down, ruling in 2018 that the claim had been filed too late ([Europa Press, via Infobae, 2023](https://www.infobae.com/espana/agencias/2023/11/12/diez-anos-de-la-caida-de-fagor-electrodomesticos-cronologia-de-una-quiebra/)). Some savers had also bought Fagor's subordinated bonds believing they were ordinary deposits ([elDiario.es, 2023](https://www.eldiario.es/euskadi/diez-anos-quiebra-fagor-electrodomesticos-tambaleo-esencia-cooperativismo-vasco_1_10612926.html)). Mondragon did protect most of the *jobs*. Of 1,736 members in the Basque plants, 577 became members of other co-ops, 320 were moved temporarily, and many more retired early or were compensated, paid for partly by doubling contributions to Mondragon's own social insurance fund (elDiario.es). It had no equivalent protection for their savings.
|
||||
- **Italy's consumer co-ops, 2014–15.** Italian consumer co-ops let their members keep savings with them, as a "social loan" to the co-op. When two co-ops in Italy's northeast failed, thousands of members' savings were frozen. At one of them, Coop Operaie di Trieste, 17,662 members held social-loan savings, and a rush of withdrawals set off the collapse ([Wikipedia, in Italian](https://it.wikipedia.org/wiki/Cooperative_Operaie_di_Trieste_Istria_e_Friuli)). At the other, CoopCa, about 3,000 members had around €27 million at stake. Under the rescue plan they could expect three-quarters back at best, and not before 2018. Neighboring co-ops voluntarily paid them up to half of their money, €13 million in all ([Il Fatto Quotidiano, 2015](https://www.ilfattoquotidiano.it/2015/08/05/crac-coopca-la-guerra-dei-soci-che-spaventa-le-altre-cooperative-del-nordest/1919384/)).
|
||||
- **Enron, 2001.** Not a co-op, but the same trap: about 63% of the money in its workers' 401(k) retirement plan was in Enron's own stock when it collapsed ([GAO-02-480T](https://www.gao.gov/assets/gao-02-480t.pdf)).
|
||||
|
||||
Three lessons come out of these. When members sense trouble, they rush to get their money out, and the rush itself can sink the firm. Savings get sold as safer than they are. And help after the fact is voluntary, partial and slow.
|
||||
|
||||
## How similar protection works elsewhere
|
||||
|
||||
- **Credit unions.** America's credit unions are member-owned, like co-ops, and they insure their members' savings through a fund they pay for themselves. Each credit union keeps a deposit worth 1% of its insured savings with the fund, and the fund aims to hold reserves of 1.2% to 1.5% of insured savings ([12 U.S.C. § 1782](https://www.law.cornell.edu/uscode/text/12/1782)). Unlike this model, that fund is backed by the full faith and credit of the United States ([NCUA](https://www.ncua.gov/support-services/share-insurance-fund)).
|
||||
- **Bank deposits.** Deposit insurance is paid for by the banks, through premiums that by law must rise with each bank's risk. The fund's legal minimum reserve is 1.35% of insured deposits, and its long-term goal is 2% ([FDIC](https://www.fdic.gov/resources/deposit-insurance/deposit-insurance-fund/); [12 U.S.C. § 1817](https://www.law.cornell.edu/uscode/text/12/1817)). It too has government backing.
|
||||
- **Unpaid wages in Europe.** Every EU country has to run a guarantee institution that pays workers' unpaid wages when their employer goes insolvent: at least three months' pay (or a set minimum over a longer reference period), up to a ceiling. Employers help pay for it unless the government covers it all, and its money has to be kept out of reach of any employer's insolvency ([EU Directive 2008/94/EC](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32008L0094)).
|
||||
- **A warning: America's pension insurance.** The federal agency that insures private pensions has charged premiums only loosely tied to each plan's risk. The Congressional Research Service put them at "about half of the market price for the insurance." Companies in trouble let their pension plans fall further behind before going bankrupt and handing them over. By 2004 the program's deficit had reached $23 billion ([Congressional Research Service, 2004](https://www.everycrsreport.com/reports/RL32702.html)). Insurance that doesn't charge for risk invites risk.
|
||||
|
||||
## The insurance pool
|
||||
|
||||
Every locked co-op belongs to a **members' savings insurance pool**. It protects three things, in this order.
|
||||
|
||||
**1. Final wages.** If a co-op fails owing its workers pay, the pool pays up to eight weeks of each worker's unpaid wages and earned time off, and then notice pay the co-op owes ([below](#notice-pay-and-severance)). For a low-paid worker, this is usually the biggest loss, and US bankruptcy law only partly protects it: unpaid wages get priority, but only up to $17,150 per worker, and only after secured lenders are paid ([11 U.S.C. § 507(a)(4)](https://www.law.cornell.edu/uscode/text/11/507)).
|
||||
|
||||
**2. Former members' payouts.** When someone leaves, their account becomes a fixed amount the co-op owes them, paid on the account's normal schedule, three to seven years after each credit was made (see [Member Capital Accounts](capital-accounts.md#how-its-paid-out)). They no longer work there and have no say in how it's run, so this is the clearest case for protection. The pool covers these unpaid payouts up to $50,000 per person.
|
||||
|
||||
**3. A protected slice of current members' accounts.** A current member's account is different. It's their share of the risk of owning the business, and they help run it. So only a slice is protected: 80% of the first $10,000. That protected slice earns the plain Treasury rate. Only the part of the account that's still at risk earns the extra two percentage points (see [Member Capital Accounts](capital-accounts.md#how-the-account-grows)). Nobody gets paid a premium for risk they don't carry, and every member keeps some skin in the game.
|
||||
|
||||
**The limits are per person**, across every co-op they've ever belonged to. An account split in a divorce or passed to an estate shares the original person's limit.
|
||||
|
||||
## Notice pay and severance
|
||||
|
||||
A worker whose co-op closes can be owed three kinds of money: pay for work already done, pay in place of the notice they should have had, and severance. The laws that require the last two overlap, so a worker could look owed the same weeks two or three times over. The model sorts it out this way.
|
||||
|
||||
- **What the laws require.** The co-op's bylaws require 30 days' notice or pay instead, with no exceptions ([When Work Runs Short](downturns.md#the-layoff-order)). Federal law requires 60 days' notice of a closing from employers with 100 or more full-time workers, or 100 or more workers putting in 4,000 hours a week between them ([20 C.F.R. § 639.3(a)](https://www.ecfr.gov/current/title-20/part-639/section-639.3)). One that closes without it owes back pay and benefits for each day short, up to 60 days, but never for more than half the days the worker was employed ([29 U.S.C. § 2104](https://www.law.cornell.edu/uscode/text/29/2104)). Courts disagree on whether that means calendar days or working days. Federal law excuses short notice when the business was seeking money that notice would have scared off, or when the cause wasn't foreseeable ([§ 2102(b)](https://www.law.cornell.edu/uscode/text/29/2102)). California requires 60 days at sites with 75 or more workers, part-time ones included, with back pay on the same terms ([Labor Code §§ 1400.5](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=LAB§ionNum=1400.5), [1401](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=LAB§ionNum=1401), [1402](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=LAB§ionNum=1402)). New York requires 90 days' notice from employers with 50 or more full-time workers there, but, like federal law, limits back pay to 60 days ([Labor Law § 860-g](https://newyork.public.law/laws/n.y._labor_law_section_860-g)). New Jersey requires 90 days from employers with 100 or more, and its penalty for short notice is four more weeks of severance rather than back pay. Other states have their own versions, and the bank's checked entry for each state lists them.
|
||||
- **Notice pay, once.** A worker gets back pay for the longest period any one law pays, not all of them added up. That's at most 60 days under the federal, California and New York laws. New Jersey's extra four weeks are severance, not notice pay. The laws figure back pay at the higher of the worker's final rate and their three-year average. The pool pays at its own covered rate ([Closing the loopholes](#closing-the-loopholes)), and any difference stays the worker's claim against the co-op.
|
||||
- **Severance: the larger of the bylaws and state law.** The standard bylaws pay a week per year worked. New Jersey's 2019 law requires the same of employers with 100 or more workers when a closing or mass layoff ends 50 or more jobs at a site, and counts federal back pay toward it ([P.L. 2019, c. 423](https://pub.njleg.state.nj.us/Bills/2018/PL19/423_.HTM)). New Jersey has amended it since, reportedly to count more workers and sites, and the bank's entry has to be checked against the current text. The bylaws never add on top of the law; they only fill in where the law pays less.
|
||||
- **What the pool pays, in order.** Out of each worker's eight weeks, first pay for work already done and earned time off, then notice pay, including the co-op's usual health contribution for those weeks. Pay from other work in the same weeks is taken off the notice pay, so nobody is paid twice for the same weeks. The pool covers notice only up to the standard bylaws' 30 days or the period the law actually requires, whichever is longer. A co-op can promise more, but the pool won't pay for it, and a longer notice period or bigger severance adopted in the two years before a failure gets neither cover nor priority. Severance isn't covered. France's fund covers it, but this pool is smaller and has nothing behind it.
|
||||
- **The pool takes over the claim it paid.** Federal law subtracts wages the employer paid for the notice days, but the pool isn't the employer, and no court has said whether its payments count. So the pool pays as an advance on the co-op's behalf, and each worker assigns to it, in writing, their notice-pay claims up to what it paid.
|
||||
- **Unemployment benefits.** In some states, pay in place of notice delays unemployment benefits for the weeks it covers. There, part of what the pool pays saves the state's fund money rather than adding to the worker's income. The pool pays notice pay in whatever way costs workers the least benefit in each state, and the bank's checked entry says what that is.
|
||||
- **Skipping notice isn't following the rules.** Directors who closed without the notice the bylaws or the law required, when no legal exception applied, are among the people the pool pursues ([Owners Are Still Workers](labor-law.md#when-wages-go-unpaid-members-can-owe-them)).
|
||||
- **Much of the severance is at risk in a failure.** Bankruptcy puts wages, severance and notice pay earned in the 180 days before filing ahead of other unsecured debts, up to $17,150 per worker ([11 U.S.C. § 507(a)(4)](https://www.law.cornell.edu/uscode/text/11/507)). Courts often treat only the part of length-of-service severance earned in those 180 days as having that priority, so a long-serving worker's week-per-year is mostly an ordinary claim that may pay little. Back pay a court awards for notice-law violations covering days after the filing can be paid ahead of that, as a cost of the case, if the court finds it won't cost other jobs ([§ 503(b)(1)(A)(ii)](https://www.law.cornell.edu/uscode/text/11/503)). A trustee that only liquidates the business may owe no federal notice at all. The pool's claim for what it paid ranks behind the worker's own unpaid claims ([When a Locked Co-op Goes Bankrupt](bankruptcy.md), point 7).
|
||||
- **A co-op buyer that keeps the workers owes no notice pay for them.** When a business is sold, its workers count as the buyer's employees from the day of the sale, so those who keep their jobs haven't lost them under federal notice law ([29 U.S.C. § 2101(b)(1)](https://www.law.cornell.edu/uscode/text/29/2101)). The failed co-op still owes notice pay to those the buyer doesn't take on, and the buyer owes notice for any later closing.
|
||||
- **Splitting doesn't avoid it.** Federal rules can treat related companies as one employer, looking at common ownership, common directors, actual control, shared personnel policies and dependence on each other ([20 C.F.R. § 639.3(a)(2)](https://www.ecfr.gov/current/title-20/part-639/section-639.3)). Co-ops in a federation share standard bylaws, a lender and a pool, so a court might count them together. That's unsettled. So the bylaws don't rely on the answer: a co-op whose group reaches 100 workers, counting companies it controls and co-ops split from it in the last three years, gives 60 days' notice as if federal law applied, and layoffs within any 90 days are added together. Neither the bank nor the pool may direct a co-op's layoffs, which would make the case for counting co-ops together stronger.
|
||||
|
||||
## How it's paid for
|
||||
|
||||
**By the co-ops, not taxpayers.** The pool has **no government backing**, for the same reason [the bank's](pilot-spec.md#the-decision-rule) debts don't. A public guarantee would be a hidden subsidy, and markets would come to assume it. That's a real difference from credit unions and banks, and it's why the pool has to be run more cautiously than they are. The charter also bars the bank from lending to the pool, buying its assets or paying its claims, so the bank can't become a backstop by the back door. No law can stop a future Congress from bailing the pool out. The best this model can do is keep the promises small enough, and the cut in a bad year plain enough, that a bailout isn't needed.
|
||||
|
||||
- **A deposit to join.** Like a credit union, each co-op keeps a refundable deposit with the pool, worth 1% of what's covered.
|
||||
- **Yearly premiums, priced as if nothing will be recovered from members' savings.** Members' accounts rank behind the co-op's lenders, so when a co-op fails, there's usually little or nothing left for them. Premiums assume exactly that. They're set by an independent actuary from a published formula, and audited every year in both directions, like [the bank's rates](pilot-spec.md#what-market-rate-means).
|
||||
- **Final-wage cover is one flat rate for everyone,** as in Germany and France. Charging the riskiest co-ops more would charge care workers most for the cover that matters most to them.
|
||||
- **Savings cover is priced on things a co-op controls,** like the size of its reserve, how much it owes, and whether its accounts are on time, measured from its audited year-end accounts. Margins set by public payment rates, like Medicaid's, aren't counted against it. The premium can rise by at most one percentage point a year, unless audited accounts show the co-op getting worse.
|
||||
- **Deferring is limited.** A struggling co-op can defer up to one year's premium, as a debt to the pool. While it does, new additions to its members' accounts aren't covered, and the pool watches its accounts monthly. Deferred premiums count as nothing in the pool's reserve, and if the co-op fails, they're paid ahead of its members' accounts.
|
||||
- **Who pays.** Current members pay for cover of their own slices, out of their account interest, in proportion to each person's protected balance. Members who take all their profit share in cash, as low-income members can, don't pay for cover they don't have. The co-op as a whole pays for final-wage cover, since everyone is covered, and for former members' payouts. Those are debts the co-op owes, and charging the premium to former members would take most of the interest on money they can't touch.
|
||||
- **Other ways to cover former members.** A co-op can instead put former members' unpaid payouts in a fully funded escrow account, or back them with a surety bond from an approved insurer. The pool is the default. If private options come in cheaper, that's a useful check on the pool's prices.
|
||||
- **The reserve is kept safe.** It's held in Treasury securities by an independent custodian, out of reach of any co-op's creditors, as Europe's rules require of wage funds. It never holds the bank's loans or anything issued by co-ops, which would fail at the same time as the co-ops it insures. The one exception is what it's owed back after a failure: when it hands the wage claims it paid to a workers' co-op buying the failed one ([When a Locked Co-op Goes Bankrupt](bankruptcy.md), point 7), the new co-op's debt to it is counted as worth nothing in the reserve until it's repaid, and can't be more than the wages the pool paid in that failure.
|
||||
|
||||
## What it should cost
|
||||
|
||||
Nobody has priced insurance quite like this, but the pieces can be estimated from what's known.
|
||||
|
||||
**Final wages and notice pay: about 0.55% of payroll.** Europe's wage guarantee funds are the closest match. Both are paid for by employers as a share of payroll:
|
||||
|
||||
- **Germany** pays up to three months of unpaid wages when an employer goes insolvent. Employers paid 0.06% of payroll in 2023 and 2024, and pay 0.15% in 2025 and 2026. After the financial crisis the rate jumped to 0.41% in 2010 ([lohn-info.de](https://www.lohn-info.de/insolvenzgeldumlage.html); [Haufe](https://www.haufe.de/id/beitrag/insolvenzgeldumlage-fuer-2026-HI17106952.html)).
|
||||
- **France** covers wages and other amounts owed to workers, including severance, up to a ceiling. Employers pay 0.25% of each worker's pay, up to about €16,000 a month, in 2026 ([Service Public](https://entreprendre.service-public.gouv.fr/actualites/A17906)). In 2009 the rate rose from 0.10% to 0.20%, 0.30% and then 0.40% within a single year ([Eurojuris](https://www.eurojuris.fr/articles/augmentation-du-taux-de-cotisation-ags-au-1er-octobre-2009-10360.htm)).
|
||||
|
||||
Both are run or required by the state and spread across every employer in the country, so their rates are a floor, not a benchmark. This pool covers less, eight weeks of wages and notice pay, but it covers young, small firms in a few industries, with nothing behind it. At 0.25% of payroll, the rate would quietly assume that a co-op that fails owes its workers, on average, about two of the eight weeks, since most businesses keep paying wages until they close. Notice pay changes that: a co-op that closes without notice owes about four weeks of it under its own bylaws, on top of the last unpaid pay period, and failing businesses rarely give notice in time. Pay from other work cuts it somewhat. So the pool starts by assuming about five of the eight weeks, at about 0.11% of payroll per week owed, which comes to 0.55% of payroll, plus a charge to build its reserve, and the actuary adjusts from there. The rate is the same for every co-op, as with wage cover in Europe, so older co-ops, which fail less often, help pay for younger ones. That's a choice, made so the newest co-ops aren't charged most for cover their workers need most. Nobody has measured any of this for small US firms. If a failed co-op owed all eight weeks, the premium would need to be about 0.9% of payroll. The pool records what it paid, and for what, in every failure.
|
||||
|
||||
**Members' savings: about 3% to 6.5% of covered amounts a year, depending on the co-op's age.** When a co-op fails, members' accounts and unpaid payouts rank behind its lenders, so the pool should expect to pay almost the whole covered amount. What matters is how often covered co-ops fail owing more than they have. Three rough numbers bracket it:
|
||||
|
||||
- **Closures of firms like these.** The Census Bureau counts every US employer that closes all its locations, each year, by the firm's age and size ([Business Dynamics Statistics, firm age by size, 1978–2023](https://www2.census.gov/programs-surveys/bds/tables/time-series/2023/bds2023_fa_fz.csv)). Selling or merging doesn't count as closing. Among firms with roughly 10 to 200 workers, the share that closed each year, averaged over 1990 to 2023, falls with age:
|
||||
|
||||
| Firm age | Closed each year |
|
||||
| --- | --- |
|
||||
| 3 | 8.0% |
|
||||
| 4 | 7.0% |
|
||||
| 5 | 6.4% |
|
||||
| 6 to 10 | 4.9% |
|
||||
| 11 to 15 | 3.7% |
|
||||
|
||||
The Bureau sorts firms by their average workforce over two years, so a firm that closes is filed at half its size. These figures count closures filed at 5 to 99 workers against firms filed at 10 to 99. Counting only closures filed at 10 to 99 would give less than half as much, which is too low. Many of the closures filed at 5 to 9 are firms that had already been shrinking, so the figures lean a little high for a healthy co-op. Figures for business locations rather than firms tell a similar story: of those opened in 2013, about 61% were still open after three years, when account cover starts, and roughly 7% to 10% closed each year after that ([Bureau of Labor Statistics](https://www.bls.gov/bdm/us_age_naics_00_table7.txt)).
|
||||
- **Not every closure is a failure.** In Census Bureau data, about a third of owners of closed businesses said the business was successful when it closed ([Headd, Census Bureau, 2001](https://ideas.repec.org/p/cen/wpaper/01-01.html)). But many "successful" closures were owners retiring or selling, which a locked co-op can't do, and members, ranking last, can lose their savings even when the lenders are paid. So the pool counts 80% of closures as claims, not two-thirds, until its own data says otherwise.
|
||||
- **Loan defaults.** Among the SBA's mature loan groups, loans making up 6% to 10% of the money lent eventually defaulted ([GAO-20-618](https://www.gao.gov/products/gao-20-618)). Spread over the life of the loans, that's roughly 1% a year. That's close to a floor: members can lose their savings in failures where the lender still gets paid.
|
||||
|
||||
Worker co-ops may do better. The best study found them about 29% less likely to close, in Uruguay ([Burdín, 2014](https://journals.sagepub.com/doi/10.1177/001979391406700108)). But the pool shouldn't count on that until its own data shows it. So it starts at 80% of the closure rate for the co-op's age: about 5% to 6.5% a year at ages three to five, when account cover starts, about 4% at six to ten, and about 3% after that. Within each age, a strong co-op, with a large reserve and little debt, pays less, a weak one more. These are starting guesses from rough data, and the pool publishes every year how its actual losses compare.
|
||||
|
||||
**That's expensive.** The protected slice of a current member's account earns only the plain Treasury rate. The premium takes most of that interest at a mature co-op, and more than all of it at a young one. That's why current members' cover comes last, is optional, and is priced on the assumption that the co-ops most likely to choose it are the weaker ones. It may never be worth offering.
|
||||
|
||||
## Starting small
|
||||
|
||||
A few hundred co-ops, all started in the same few years, can't spread this risk well. They're all at the riskiest age, and one recession hits them together. So cover comes in stages:
|
||||
|
||||
- **From the start: final wages and former members' payouts,** for every covered co-op. These protect people with no say in how the co-op is run, and final wages are the biggest loss for low-paid workers.
|
||||
- **Current members' slices come later,** once the pool has seen about 1,000 years of co-op experience (say, 200 co-ops for five years) and its savings reserve has reached its target. Then each co-op can choose it, but only in a yearly window, based on audited accounts, with cover starting two years later. The pool can refuse a co-op that's showing warning signs. A co-op that opts out can't opt back in for five years. That stops co-ops from joining only when they're in trouble.
|
||||
- **Limits follow the reserve, not the calendar.** Every year, the limits are set by a published formula from the reserve the pool actually has, compared with what a bad recession would cost ([the formula](#how-big-the-reserve-has-to-be) is below). If covered amounts grow faster than the reserve, the limits wait.
|
||||
|
||||
## How big the reserve has to be
|
||||
|
||||
Failures come in waves, and the waves last. US business bankruptcy filings ran about 22,000 a year in 2018 and 2019. In the last big recession they rose to about 43,500 in 2008, 60,800 in 2009, 56,300 in 2010, 47,800 in 2011 and 40,100 in 2012 ([American Bankruptcy Institute](https://abi-org.s3.amazonaws.com/Newsroom/Bankruptcy_Statistics/Total-Business-Consumer1980-Present.pdf); [US Courts](https://www.uscourts.gov/sites/default/files/statistics_import_dir/1209_f2.pdf)). Over those five years, that's about six normal years' worth of extra failures. And most small businesses that close never file for bankruptcy at all.
|
||||
|
||||
Closures tell a milder story. In the same Census data, closures of firms like these rose by about half a point in 2008 and 2009, and by about a point for firms three to five years old, and were back down by 2010. The early 1990s were worse. What rises in a recession is the share of closures that leave debts unpaid, which is what bankruptcy filings count, and that's what the pool pays for. So the real wave for the pool is somewhere between: closures say almost no extra years, bankruptcies say six.
|
||||
|
||||
A pilot's co-ops are all young and concentrated in a few industries and regions, which makes a wave worse. So:
|
||||
|
||||
- **The target for savings cover is six extra years' worth of expected claims while the pool is young,** meaning until it has 1,000 years of co-op experience and no one industry in one state holds more than a quarter of what's covered. After that it's four years' worth, about 15% of covered amounts for mature co-ops. That compares with the 1.2% to 2% that government-backed funds aim for. Nothing stands behind this pool, so its reserve has to carry a long recession on its own. It doesn't count on reinsurance, which may not exist for this kind of risk.
|
||||
- **For final wages, about 10% of covered wages,** by the same reckoning.
|
||||
- **Building it takes time.** Premiums carry an extra point a year for savings cover, and an extra 0.1% of payroll for wage cover, until the targets are reached. For savings cover that takes about fifteen years for mature co-ops if the pool doesn't grow, and a young pool won't reach its target during the pilot at all. That's why limits follow the reserve.
|
||||
- **Surcharges wait for the recovery.** After a bad stretch, premiums can rise by up to one point of covered amounts for up to three years. But the surcharge starts only once national unemployment has stopped rising, so it doesn't hit co-ops at the bottom of a recession. The surcharge is paid by the co-ops that survived. Until then, if claims outrun the reserve, each person's first $2,000 is paid in full, and everyone is paid the same share of the rest, as below.
|
||||
- **Concentration is limited by limits, not refusals.** If one industry in one state grows past a quarter of what's covered, cover limits for new additions there are lowered. No co-op is refused cover because of its industry.
|
||||
|
||||
**The formula for limits.** The inputs are written into the charter so the actuary can't pick friendlier ones: the Census closure rates by age for 1990 to 2023, at 80%, until the pool has 1,000 years of its own experience. After that, its own record can lower the rates by at most a quarter. An outside actuary signs off on the inputs, not just the result.
|
||||
|
||||
- **The reserve has a planned path:** what the build charges should have produced by now if claims had run as expected. Only the reserve counts, not the refundable deposits or future premiums, which are priced to pay normal claims.
|
||||
- **If the reserve falls behind the path,** limits for new additions fall in proportion, and the cut applies back to everything added since the last yearly valuation, so nobody can rush additions in ahead of it. Amounts covered before that keep their cover.
|
||||
- **Limits come back slowly:** by at most a tenth a year, and only after two years on the path.
|
||||
- **No switching back and forth.** A co-op that moves its former members' cover to escrow or a surety bond can't return to the pool for five years, and the pool prices it when it does.
|
||||
|
||||
**A bad fifth year.** Suppose a recession starts in the pilot's fifth year. The covered co-ops are three to five years old, priced at about 5.7% a year on average. The reserve is small, about 3% of covered amounts, because most co-ops joined recently. Five bad years at that rate, plus four extra years' worth, come to about 51% of covered amounts. Premiums bring in about 27%, counting a fifth of the paying co-ops lost along the way. With the reserve, that pays about 58 cents on every dollar claimed. With six extra years' worth, or if every closure turns out to be a claim, it's closer to 45 to 50 cents. The industry with the most co-ops is also stressed on its own, at twice its normal rate. For final wages and notice pay, if a failed co-op owes the five weeks the rate assumes, the pool pays about three-quarters. If it owes all eight, it pays a bit under half. So in its first years the pool is a partial promise. Each member's yearly statement shows what the pool would pay per dollar in this stress case, as it stands that year.
|
||||
|
||||
**What it costs, in the standard example.** A mature 40-person co-op might have $300,000 of protected account slices (at most $8,000 a member, and less for members who take their share in cash), $200,000 of former members' payouts outstanding, and about $2 million a year of payroll, with eight weeks of it, about $300,000, covered by final-wage cover.
|
||||
|
||||
| Cover | Premium | A year |
|
||||
| --- | --- | --- |
|
||||
| Final wages and notice pay | 0.55% of payroll, plus 0.1% to build the reserve | about $13,000 |
|
||||
| Former members' payouts | 3%–4% for a co-op past its tenth year, plus 1 point to build the reserve | $8,000–$10,000 |
|
||||
| Current members' slices, once offered (after the reserve target is reached), if the co-op chooses | 3%–4% | $9,000–$12,000 |
|
||||
| **All three** | | **$30,000–$35,000** |
|
||||
|
||||
That's about 5% to 6% of the $600,000 annual profit in the [capital accounts example](capital-accounts.md#where-each-years-profit-goes), or about 3.5% to 4% without cover for current members. A strong co-op would pay less, a weak one more. Once the reserve reaches its target, the extra charges stop. The real rates will come from the pool's actuary and its own experience.
|
||||
|
||||
## Who runs it, and how claims are paid
|
||||
|
||||
- **A federal charter.** The pool is created by the same federal law as the bank, with a named federal regulator, capital standards, and a clause saying state insurance law doesn't apply to it. Otherwise it would need a license in every state, and federal law generally leaves insurance to the states unless Congress says otherwise.
|
||||
- **An independent board.** Most of its members are independent (actuaries, former insurance regulators, people with no stake in any co-op), appointed by a body separate from the bank, for fixed staggered terms, and removable only for cause. A minority are elected by covered workers. None represent the co-ops that pay premiums, so the biggest co-ops can't keep their own premiums low at everyone else's expense. Board members step aside from anything involving their own co-op. Limits on premiums are written into the charter, and a co-op can appeal its rate to the independent adjudicator.
|
||||
- **Checked every year.** An outside actuary publishes an opinion on the pool's reserves, and federal auditors audit it. The pool and the bank are stress-tested together, since the same recession would hit the bank's loans and the pool's claims at once. If the reserve falls below its target, the board publishes a plan to restore it within 90 days, and limits stop rising until it does.
|
||||
- **One pool, one register.** Federations can run their own pools only once each meets the national pool's reserve ratio on its own, since pools of co-ops all lose in the same recession. There's one national register of everyone covered, so the per-person limits hold across pools.
|
||||
- **Automatic payment.** When a covered co-op fails, the pool pays from the co-op's own member register and payroll records. Nobody has to know to file a claim. Fagor's former workers lost their case partly because they filed too late. Disputes go to an independent adjudicator. Final-wage payments are wages, so the pool or the liquidator pays them through payroll, with the usual withholding.
|
||||
- **The pool steps into the workers' shoes.** Once it pays wages, it takes over the workers' priority claims in bankruptcy ([When a Locked Co-op Goes Bankrupt](bankruptcy.md)) and under federal and state notice and wage laws, so it recovers what it can. Workers are never paid twice for the same weeks ([Notice pay and severance](#notice-pay-and-severance)). Federal law and some states can make a company's officers or owners personally liable for unpaid wages, and New York appears to make every co-op member liable ([Owners Are Still Workers](labor-law.md#when-wages-go-unpaid-members-can-owe-them)). In a co-op, those are workers, so the pool doesn't pursue ordinary members or officers who followed the rules, only people who hid losses or kept others working after a missed payroll.
|
||||
- **Stepping in early.** Every covered co-op sends the pool quarterly accounts. If warning signs appear, the pool can stop covering *new* additions to accounts until the co-op recovers, which stops members from piling up covered savings in a sinking firm. That freeze also applies to anyone who leaves after it starts, so members can't escape it by quitting.
|
||||
|
||||
## Closing the loopholes
|
||||
|
||||
- **No paying out phantom profits.** Before paying, the pool's own auditor or the liquidator rechecks the last three years of accounts, and for former members, the years since they left. Losses that were hidden or booked late are charged to accounts first. A covered balance can never be more than the allocations reported to the IRS.
|
||||
- **No quitting to upgrade your cover.** A current member's slice is covered up to $8,000, and a former member's payout up to $50,000. So for the first three years after someone leaves, their payout is covered only like a current member's slice. Anyone who left before a failure has their balance recalculated to share the losses of the years since they left. Someone rehired within three years goes back to current-member cover. And if an unusual share of the co-op's accounts leaves in one year, those departures get only current-member cover.
|
||||
- **No stuffing accounts.** Amounts added to accounts in the year before a failure are covered only up to the co-op's normal split, meaning its average over the previous five years.
|
||||
- **No inflated final wages.** Covered pay is the lower of a worker's average over the previous 12 months and what appears in the co-op's payroll tax filings. Paid time off counts only at the co-op's normal accrual. In a co-op's first year, final-wage cover is four weeks, not eight.
|
||||
- **No paying insiders first.** Loans that directors or members guaranteed personally, repaid in the year before a failure while wages went unpaid, are clawed back.
|
||||
- **No cover for brand-new shells.** Account cover starts after a co-op's first three years of checked accounts and an audit by the pool itself ([What the Rules Cost a Small Co-op](compliance.md#what-a-review-can-and-cant-stand-in-for)), and every covered co-op must pass the same checks as bank borrowers: real members doing most of the work, verified against payroll records (see [Guarding the Pilot](pilot-safeguards.md#the-hidden-boss)).
|
||||
- **No trading claims.** Covered claims can't be sold or transferred, except in a divorce or to an estate.
|
||||
- **No underreporting.** Payouts are based on the lower of what the co-op reported and what it actually owed. Unpaid premiums are charged to directors and to uncovered balances, not to protected ones, and directors who file false reports are personally liable.
|
||||
|
||||
## What the pilot records
|
||||
|
||||
The pool files its pricing model before the first co-op is covered, and each year publishes its predicted losses against its actual ones. For every claim, it records the covered amount at failure, what was recovered, and how long payment took, by type of cover. It also publishes which co-ops deferred premiums and which chose current-member cover, and compares wage losses with those at the matched conventional firms in [the pilot](pilot-spec.md#who-co-ops-are-compared-against). That's how the next design gets real numbers instead of these.
|
||||
|
||||
## No selling savings as something they're not
|
||||
|
||||
Both Fagor and the Italian co-ops got into trouble partly because members' money was held in things that looked like bank deposits. So:
|
||||
|
||||
- **A locked co-op can't take savings deposits** from its members, or sell them bonds or other investments. A member's only money in the co-op is their capital account.
|
||||
- **Every member gets a plain statement each year** of how much of their account is protected, how much isn't, and what the pool can and can't promise.
|
||||
|
||||
## What it can't promise
|
||||
|
||||
- **A wave of failures could outrun the pool.** Co-ops in the same region or industry tend to fail together, in the same recession. The reserve target is sized for a long recession, but it takes years to build, and in the pilot's early years a bad recession would leave the pool paying roughly 45 to 60 cents on the dollar. If claims exceed what the pool has, it pays each person's first $2,000 in full and everyone the same share of the rest at once, then pays the rest as money comes in. That rule is published in advance, and members are told about it every year. This model gives the pool no government backing, so it can't promise more. **It's insurance, not a guarantee.**
|
||||
- **It starts small.** It will be weakest in its early years. That's why coverage phases in, current members' slices come later, and account cover starts only after three years.
|
||||
- **Members pay for it, and it isn't cheap.** Premiums come out of account interest and profit. Insuring current members' savings could take most of the interest on the protected slice, which is why co-ops can choose not to. That's the honest price of making savings safer.
|
||||
- **Bankruptcy law may not cooperate.** Courts can push former members' claims to the back of the line, behind other creditors, which makes the pool's recoveries even smaller. That's another reason it assumes nothing comes back.
|
||||
- **The legal form needs Congress.** Without the federal charter, a pool like this is insurance under state law, needing a license in every state. Whether capital accounts count as securities is also unsettled ([Member Capital Accounts](capital-accounts.md#are-accounts-securities)). Both are listed in [Open Problems](open-problems.md).
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
title: "After a Pass: How Big, How Fast"
|
||||
section: Growing the Sector
|
||||
order: 68
|
||||
summary: Even if the pilot passes, a co-op sector big enough to change how other employers treat workers is decades away nationally, so growth comes in capped stages sized to audited demand, and the labor-market effect gets its own test in a few local markets.
|
||||
---
|
||||
|
||||
# After a Pass: How Big, How Fast
|
||||
|
||||
The pilot tests one thing: can worker co-ops compete, survive and repay market-rate loans ([The Test](the-test.md))? That's the case for the bank, and it stands on its own: people get to govern their own workplaces, and the public gets its money back. The bigger hope is different. If co-ops become common enough, conventional employers would have to treat workers better or lose them to co-ops. That's the **labor-market effect**. The pilot can't test it, because about 400 co-ops employing perhaps several thousand people won't move anyone's wages.
|
||||
|
||||
This page works through what comes after a pass: how big the sector would have to get, how long that could take, how to grow without lowering standards, and how to find out whether the effect is real without waiting decades or promising it in advance.
|
||||
|
||||
## How big is big enough?
|
||||
|
||||
Nobody knows. But three kinds of evidence give a sense of it, and they don't all point the same way.
|
||||
|
||||
**When big retailers raised their own pay, other employers didn't follow.** From 2014 to 2023, large retailers like Amazon, Walmart and Target set their own company-wide minimum wages. Those covered over 3 million jobs. A study using payroll records for thousands of firms found the policies raised pay and cut turnover at the companies that adopted them. But the effect on other employers' pay, even ones that hired from the same workers, was "precise" and "economically negligible." The authors' explanation: the retailers mostly kept their workers longer rather than hiring more, and other employers could still find plenty of people ([Derenoncourt and Weil, 2024](https://www.nber.org/papers/w32546)).
|
||||
|
||||
An earlier version of the Amazon research did find that other employers raised pay too, and it's still widely quoted. Its own authors later found the result came from a flaw in their method, a statistical effect called mean reversion, and with revised methods they found no evidence of it ([Derenoncourt and others, revised 2022](https://www.nber.org/papers/w29425)).
|
||||
|
||||
**When one employer hired a lot in a tight market, others did follow.** The same study describes a different case. When Veterans Affairs hospitals raised pay for registered nurses, nurses' pay at nearby hospitals rose too. The authors think the difference is that nurses are a small, concentrated market, while low-wage retail work is not. So an employer that adds real demand for scarce workers can move others' pay, and one that just keeps its own workers longer mostly doesn't. New co-ops create new jobs, which puts them closer to the nurses' case, but only if they're a big enough share of the market.
|
||||
|
||||
**Unions once lifted pay for non-members, at much higher densities.** In 1977, about 22% of American private-sector workers were in unions. By 2015 it was 7%. One study estimates that if unions were still as common in each occupation and region as in 1977, non-union men would earn about 6% more, around $3,200 a year, and non-union women about $900 more. That's the "threat effect": employers paid more to keep unions out. It showed up where unions were dense in a particular occupation and region, not as a national average. The study compares places and times, so it can't prove cause and effect ([Denice and Rosenfeld, 2018](https://doi.org/10.15195/v5.a23)). And co-ops don't work the same way. A union threat happens inside your current job. A co-op only helps workers who can leave for one or buy their own workplace. So this suggests a scale, not a target: an effect like that came with about one private-sector worker in five.
|
||||
|
||||
**The biggest co-op sectors are still small shares.** Mondragon is the largest employer in the Basque Country ([Co-operative News](https://www.thenews.coop/mondragon-worker-co-op-federation-reports-sales-of-e11-213bn/)), and about 30,660 of its 70,000 workers were there in 2024, not all of them members ([Wikipedia, citing Mondragon's 2024 annual report](https://en.wikipedia.org/wiki/Mondragon_Corporation)). That's about 3% of the roughly 995,000 people working in the Basque Country ([Eustat, via Orain](https://orain.eus/es/actualidad/economia/2025/01/21/las-personas-ocupadas-bajan-0-2-y-tasa-de-paro-se-mantiene-en-7-1-pero-con-1100-parados-menos/)), after almost 70 years. France, with one of the best-supported worker co-op sectors in the world, counted 87,699 people working in its worker co-ops and a related form of community co-op in 2024 ([ESS et société, reporting CG Scop](https://www.ess-et-societe.net/Les-Scop-et-Scic-poursuivent-leur-croissance-avec-87-699-salaries-4-et-un-CA-de-10-2-milliards-d-EUR-6)).
|
||||
|
||||
So the honest answer: any effect is likely to show up locally, in particular jobs in particular places where co-ops are a big share, long before it shows up in national numbers. Whether it shows up at all is what the local test below is for.
|
||||
|
||||
## How long could it take?
|
||||
|
||||
America had about 136 million private-sector jobs in August 2026 ([Bureau of Labor Statistics](https://www.bls.gov/news.release/empsit.t17.htm)). One percent of that is about 1.36 million.
|
||||
|
||||
Nobody knows how many people work in American worker co-ops today. The sector's census counted about 751 co-ops in 2023, triple the number a decade earlier ([NCEO summary of the Democracy at Work Institute census](https://www.nceo.org/employee-ownership-blog/democracy-at-work-institute-releases-its-2023-worker-cooperative-state-of-the-sector-report)), but only 242 firms answered its latest survey, too few to add up workers ([US Federation of Worker Cooperatives](https://www.usworker.coop/census/)). So suppose, as a round assumption, that 20,000 people work in them by the pilot's year 12. To reach 1% or 3% of today's private jobs:
|
||||
|
||||
| If the sector grows by | 1% of private jobs in | 3% (Mondragon's Basque share) in |
|
||||
| --- | --- | --- |
|
||||
| 12% a year, about the pace at which the number of co-ops tripled over the last decade | about 37 years | about 47 years |
|
||||
| 25% a year, twice that pace, every year | about 19 years | about 24 years |
|
||||
|
||||
These are minimums. The number of jobs keeps growing, so the target keeps moving. The 12% pace comes from the number of co-ops, not the number of workers. And fast growth is hard to sustain. Starting from fewer than 20,000 workers makes every figure longer.
|
||||
|
||||
So nationally, even a clear success likely means decades before co-ops are common enough to matter to most workers. That's the price of not forcing it, and I'd rather say so now than have someone discover it later.
|
||||
|
||||
## What fast growth has looked like
|
||||
|
||||
**Rural electric co-ops grew fast, in conditions this model doesn't have.** In the mid-1930s, nine in ten rural American homes had no electricity. By 1953, over 90% of farms had it. The federal Rural Electrification Administration lent to co-ops of farmers who wired the countryside themselves, and two years after it started there were already 350 co-op projects in 45 states ([EH.net](https://eh.net/encyclopedia/rural-electrification-administration/); [America's Electric Cooperatives](https://www.electric.coop/our-organization/history)). Two things made that speed possible. Private utilities had refused to serve those areas, so the co-ops had no competitors. And the loans were cheap: 25 years to repay, and from 1944 an interest rate fixed at 2%, whatever the market charged. This model has neither. Co-ops compete with established firms in every industry, and they pay market rates. The electric co-ops also carry a warning. They built defenders in almost every congressional district, and critics can fairly say those defenders kept cheap loans flowing long after the original job was done ([Making It Last](durability.md)).
|
||||
|
||||
**Employee ownership with tax breaks grew big, in a different form.** In 2023, about 6,400 American companies had employee stock ownership plans (ESOPs), with 10.9 million active participants ([NCEO](https://www.nceo.org/research/employee-ownership-by-the-numbers)). That's equal to roughly 8% of private-sector jobs, though participants aren't the same as jobs, and a plan can own only a small slice of its company. It's the biggest American case of ownership spreading through ordinary finance, and it took 50 years and large federal tax breaks. ESOPs don't give workers one member, one vote. I haven't found any study of whether they lift pay at other employers.
|
||||
|
||||
## Growing in stages
|
||||
|
||||
If the pilot passes, the charter sends Congress a fast-track vote on more capital ([The Pilot, in Numbers](pilot-spec.md#the-decision-rule)). Here's how the growth after that is meant to work:
|
||||
|
||||
- **Each stage is sized to audited demand, not a target.** The bank can ask for enough capital to fund the good applications it had to turn away for lack of money, plus at most 20%. Only unique applicants count, only those that met the bank's filed lending standards, and the count is certified by the independent reviewers, not the bank ([Making It Last](durability.md)). Delays don't count. There are still no lending or job targets. An institution with a target lowers its standards to hit it, which is what sank Venezuela's co-op push ([The Pilot, in Numbers](pilot-spec.md#two-warnings-from-real-life)).
|
||||
- **Capped growth.** No stage can more than double the bank's capital. A bank that suddenly has to find ten times as many borrowers will lower its standards without meaning to.
|
||||
- **Demand can't be starved, either.** Applications and approval rates are published against the pilot's own. A hostile administration can't cut outreach, tighten what counts as "good," and then claim nobody wants to borrow, because the reviewers already re-score rejected applications every year and publish how many met the standards anyway ([Making It Last](durability.md)).
|
||||
- **The same rules at every stage.** Market rates, the same underwriting, the same limits by industry and county, and no federal guarantee on anything the bank borrows.
|
||||
- **Growth follows the evidence for each kind of work.** Lending to co-ops in lower-paid work is sized from that group's own result, and the part held for the lower tier from that tier's result. If the pilot had too few of them to tell, that lending can't grow faster than the rest ([Who the Bank Lends To](who-gets-loans.md#results-by-group)).
|
||||
- **The same measures, published for every new group of co-ops.** Each later round of capital is its own vote in Congress, so every Congress sees how the newest co-ops are doing, not just the pilot's. A passed test shows that co-ops funded one way, at one size, could compete. It doesn't show the same is true at a larger volume, when the bank has to reach further for borrowers.
|
||||
- **The full public cost, every year.** The bank publishes its all-in cost to taxpayers per co-op job: losses, running costs, all business help, and what the public's capital could have earned in Treasury bonds instead.
|
||||
|
||||
## Testing the labor-market effect
|
||||
|
||||
The pilot can't test it. But once the sector grows, it can be tested locally, without waiting decades and without deciding anything in advance.
|
||||
|
||||
The model is a French experiment that started in 2007. Researchers wanted to know whether job counseling for young unemployed people really created jobs or just moved them from one job seeker to another. They randomly chose what share of eligible people would get counseling in each of 235 local labor markets: 0%, 25%, 50%, 75% or 100%. The counseled did find stable jobs faster. But the gains were short-lived, and partly came at the expense of people in the same markets who weren't counseled, especially in weak markets ([Crépon and others, 2013](https://www.nber.org/papers/w18597)). A test of individuals alone would have missed that.
|
||||
|
||||
The same idea can test co-ops.
|
||||
|
||||
### Where
|
||||
|
||||
- **A few kinds of work** where the pilot showed co-ops can compete. At least one should be work where employers really set pay. In home care, for example, pay depends heavily on what Medicaid reimburses, so employers may not be free to respond. Where that's true, changes in reimbursement are measured and controlled for.
|
||||
- **Local labor markets drawn by lottery,** from a pool whose rules are written into the charter: labor markets defined by commuting patterns, with 2,000 to 4,000 private-sector jobs in the chosen kind of work (big enough to measure, small enough that co-ops could plausibly hold a real share), and local governments that opt in. A market counts as opted in only if the governments covering most of its workers do. The draw is public and made within states, so markets are compared with others under the same state minimum wage and Medicaid rates. Markets that border a high-help market are left out of the comparison, because workers and employers cross those lines. Its random seed is published in advance. Members of Congress can't add or remove places.
|
||||
- **Help or none.** Half the markets get a high level of outreach and [help before the first loan](before-launch.md), including outreach to owners who might sell to their workers ([When an Owner Wants to Sell to the Workers](buyouts.md)). The other half get none. The French experiment used several levels, but a middle level would cost a third of the budget and, with this few markets, add almost nothing to what the test can detect. The help runs for five years. The charter sets a minimum budget, so a hostile administration can't quietly starve it: $300 a year for each job in the chosen kind of work in the market, in today's dollars, rising with inflation. That's my first guess, sized in [the numbers below](#the-numbers-in-a-first-draft). It's paid from its own appropriation, not from loan money, and counted in the bank's published cost per co-op job.
|
||||
|
||||
### What doesn't change
|
||||
|
||||
**Lending.** Loans in the chosen markets go through the same underwriting, at the same rates, under the same limits by industry and county as everywhere else. Only the outreach and help are concentrated. If the concentration the test needs would break those limits, the limits win, and the test is smaller.
|
||||
|
||||
### What gets measured
|
||||
|
||||
- **What happens to co-ops.** Their share of the work in each market, with a minimum share filed in advance that counts as "reached." New co-ops and buyouts of existing firms are counted separately. A buyout changes who owns a job but adds no new demand for workers, and the evidence above suggests new demand is what moves other employers' pay. Co-ops that move in from outside the market don't count.
|
||||
- **What happens to conventional employers** in the same kind of work: pay, turnover, hours, and working conditions where records exist, like injuries.
|
||||
- **Who gains.** Results by gender, race and ethnicity, and co-op membership compared with the local workforce. Most home care and cleaning workers are women, many of them Black, Latina or immigrant workers, and the union evidence found much smaller gains for women.
|
||||
- **Whether co-ops actually let people in.** Co-op job openings, how long it takes new hires to become members, and the share of work done by non-members. A large co-op sector that doesn't hire is no outside option for anyone.
|
||||
- **The costs.** Total jobs in the market, conventional firms closing, and prices where they can be measured, so that higher pay bought with lost jobs or higher prices shows up.
|
||||
|
||||
**The data.** Government wage records, like the Census Bureau's Quarterly Workforce Indicators, which report pay, hiring and turnover by industry, place, gender, race and ethnicity. The detailed records are used only inside secure federal research centers, and nothing is published that identifies a business. Co-op borrowers agree, as a loan condition, to have their records linked. Home care and cleaning include a lot of work that wage records miss, like independent contractors and households paying workers directly, so worker surveys fill in. An independent team named in advance does the analysis, and its data access can't be revoked by the administration. Pay is tracked for at least five years, because an incumbent could raise pay briefly to stop co-ops forming and cut it once they're gone.
|
||||
|
||||
### Filed in advance
|
||||
|
||||
The markets, the kinds of work, the measures, the analysis and a power calculation are filed before the draw. The power calculation states the smallest effect the test could detect. If that's bigger than any effect that's plausible, the test isn't run, because an expensive test that can't find anything isn't worth running.
|
||||
|
||||
### The numbers, in a first draft
|
||||
|
||||
These are my own rough figures, from public data. The filed version is redone by the independent team with the detailed records. On these figures, **the test fails its own rule.**
|
||||
|
||||
- **How noisy pay is.** I took the Bureau of Labor Statistics' county pay records for home health care, services for elderly and disabled people, and commercial cleaning ([BLS county data, 2014–2024](https://www.bls.gov/cew/downloadable-data-files.htm)). In counties with 2,000 to 10,000 private-sector jobs in the work, average weekly pay rose by quite different amounts over five years, both from 2014 to 2019 and from 2019 to 2024. Comparing counties in the same state, the typical spread (the standard deviation) was about 8% to 11%. Past growth didn't predict future growth. Weekly pay mixes hourly pay with hours worked and with who's employed, so the Census records, which can follow the same workers over time, could be less noisy. I haven't been able to check that.
|
||||
- **The smallest effect the test could find.** With a 9% spread and 100 markets, 50 with help and 50 without, the test would reliably detect a difference of about 5% in pay at employers other than co-ops. "Reliably" means an 80% chance of finding an effect that size, with a 5% chance of a false alarm. There may not be 100 markets to draw from. I count only about 100 counties of the right size in each kind of work, in states with at least two, before any opt out.
|
||||
- **What's plausible.** The only guide is the union evidence above: men's pay about 6% higher where unions were about 15 points more common, or about 0.4% for each point. Women's gain was about $900 a year against men's $3,200, and most home care workers are women. It came from comparing places, not from an experiment, and a co-op's pull may not grow point by point the way this assumes. At a 10% co-op share, the men's figure suggests an effect of about 4%, and the women's about 1%.
|
||||
- **So the rule says no, for now.** Both of those are below 5%. The filed rule compares the smallest detectable effect with the union figure, weighted by how many men and women do the chosen work. On public numbers it fails for every kind of work I checked, and fails badly for home care. The test only runs if the independent team, using the detailed records, shows it passes. If it never passes, the local test isn't run, and the page says what that means: the labor-market effect stays untested. A smaller effect, like the union one for women, is probably too small for any affordable local test to see.
|
||||
- **What counts as "reached."** If the test does run, help markets must average at least 10% of the hours in the chosen work done by workers at new co-ops, measured in year five by the independent team from wage records. That's ambitious. The pilot's own help is judged against about 32 extra co-ops from about 250 founding groups ([Help Before the First Loan](before-launch.md#the-cost-ceiling)), roughly one in eight. At that rate, 10% of a 2,000-job market, about seven co-ops of 30, would take something like 55 founding groups in one place. So the test also runs only if the pilot's results make that reachable.
|
||||
- **A checkpoint, so the money isn't wasted.** If help markets average under 4% at the end of year three, the help stops and the result is "not tested." The same team audits whether the help was actually delivered. If it wasn't, the result says so, and it's reported as a broken promise, not as co-ops failing. Pay in the markets is still tracked for the full five years.
|
||||
- **What it costs.** In a 2,000-job market, seven extra co-ops at the pre-launch help's own limit of $400,000 each cost about $2.8 million, or $280 a job a year over five years. Hence $300 a job. With 50 help markets averaging 3,000 jobs, that's about $225 million over five years. None of it is lending. Loans still go through the bank's normal rules.
|
||||
- **One main measure.** The main result is filed in advance: the pay of workers who stay in their jobs at employers other than co-ops, in one kind of work. Everything else, including other kinds of work, groups of workers, new hires' pay and the share of workers below a low-pay line, is reported with a correction for testing many things at once. Employers that set pay across many markets, like home care chains, are reported separately, since they could carry any response into the markets without help.
|
||||
|
||||
**This test decides nothing by itself.** It isn't part of the pass-or-fail rule, which stays about whether co-ops can compete. Its results are published for voters and Congress to weigh. If co-ops never reach the filed share in the chosen markets, the result is "not tested." If they do and conventional employers don't change, the result is "no effect," and it's reported that way.
|
||||
|
||||
## Compared with wage boards and unions
|
||||
|
||||
The labor-market effect is about raising pay for workers who never join a co-op. Other policies go at that directly. Here's how they compare, on the best evidence I could find. The comparison isn't like for like, and I say where.
|
||||
|
||||
**Wage boards.** A wage board sets minimum pay for one kind of work, usually with workers and employers at the table. Two recent ones:
|
||||
|
||||
- **California fast food.** Since April 2024, restaurants in fast-food chains with more than 60 locations nationally, franchised ones included, have had to pay at least $20 an hour ([California Labor Code 1474](https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=LAB§ionNum=1474.)). California's fast-food and similar restaurants had about 569,000 jobs in 2023 at an average of about $26,900 a year, and about 554,000 jobs at about $30,300 in 2025 ([BLS county and state pay records](https://www.bls.gov/cew/downloadable-data-files.htm)). Not all of them are at covered chains, and those raw changes also reflect everything else going on in California.
|
||||
- **Pay.** The studies agree pay went up because of the law. Compared with similar places, about 7% on average in one careful estimate ([Dube, 2026](https://www.nber.org/papers/w35171)), and 8% to 9% for covered workers in a Berkeley report ([Reich, 2025](https://irle.berkeley.edu/publications/brief/effects-of-the-20-california-fast-food-minimum-wage/)).
|
||||
- **Jobs.** They disagree. One team finds about 3% fewer fast-food jobs than there would have been, around 18,000 ([Clemens, Edwards and Meer, 2025](https://www.nber.org/papers/w34033)). Dube's main estimate works out to roughly 1% fewer, and his other methods range from small losses to small gains. He also finds workers quit much less often. The Berkeley team, using phone location data at each restaurant, finds no job loss ([Reich and Sosinskiy, 2026](https://irle.berkeley.edu/publications/press-release/most-comprehensive-study-yet-finds-no-job-losses-from-californias-20-fast-food-minimum-wage/)). Another team using restaurant-level data finds losses concentrated at small chains, which also opened fewer new locations and closed more ([Melo, Neumark, Sigaud and Wagner, 2026](https://www.nber.org/papers/w35749)).
|
||||
- **Prices.** Prices for all eating out in four California cities rose about 3.3% to 3.6% more than elsewhere by the end of 2024 ([Clemens and others, 2026](https://www.nber.org/papers/w34990)). The Berkeley report, looking at fast-food prices, finds about 1.5%, or six cents on a four-dollar hamburger. They measure different things.
|
||||
- **Minnesota nursing homes.** Minnesota's nursing home board set minimums of $19 to $27 an hour depending on the job. They took effect on September 10, 2026 ([Minnesota Department of Labor and Industry](https://www.dli.mn.gov/nhwsb)). Medicaid pays for much of nursing home care, so the raise needed money from the legislature and federal approval of the state's Medicaid plan. That pushed the start back from January 2026 ([the board's 2025 report](https://www.dli.mn.gov/sites/default/files/pdf/nhwsb_annual_report_2025.pdf)). I couldn't find the dollar cost.
|
||||
|
||||
**Unions.** The federal agency that protects the right to organize, the National Labor Relations Board, cost $294 million in 2026 ([NLRB budget request](https://www.nlrb.gov/sites/default/files/attachments/pages/node-155/performance-budget-justification-2027.pdf)), about $2 a year per private-sector job. Where states bargain with home care workers, the raises come out of Medicaid, like Minnesota's. The union evidence above is the main reason to think one employer's pull on others' pay exists at all.
|
||||
|
||||
**What each costs, roughly.** These aren't the same kind of cost, so here they are side by side rather than netted:
|
||||
|
||||
| | Wage board (California) | Co-op local test |
|
||||
| --- | --- | --- |
|
||||
| Who pays | Customers every year, including low-income ones, plus owners' margins and possibly some lost jobs and openings, mostly at small chains | Taxpayers, once |
|
||||
| How much | About $1,900 a year per covered job (7% of $26,900), for as long as the floor lasts | About $1,500 per job in the market ($300 a year for five years), none of it lent back |
|
||||
| What it buys | A raise within about two years | A test, which may never run, of whether co-ops at a tenth of the work lift others' pay. On the union evidence, perhaps 4% for men and 1% for women, later, if at all ([The numbers, in a first draft](#the-numbers-in-a-first-draft)) |
|
||||
|
||||
If a co-op effect like that did happen and lasted, a one-time $1,500 would be a good buy. But it depends on co-ops reaching a share of the work that this page already doubts they can reach. So as a way to raise pay across a whole kind of work, a wage board is faster, more direct, and cheaper for the government's budget, though its effect on jobs is still disputed. The labor-market effect isn't a reason to prefer co-ops for that job.
|
||||
|
||||
**What co-ops do that these don't.** A wage floor raises pay. It doesn't give workers a vote or a share of the profits, it doesn't let them keep what they build, and it lasts only as long as the budget and the politics behind it. Minnesota's slipped for funding. Co-ops give ownership and a vote, but they can't pay more than the work earns ([Ownership and the Floor](the-floor.md)). That's why the model needs both. The case for the bank doesn't rest on the labor-market effect. It rests on whether co-ops can compete and repay ([The Test](the-test.md)). Where they touch:
|
||||
|
||||
- **Co-ops don't compete by paying under the floor.** Co-ops pay any floor that covers them, like any other employer, and neither the bank nor the federation seeks exemptions for them. A co-op outside a floor's reach, like an independent restaurant under California's law, still publishes its pay next to the local floor.
|
||||
- **A floor can hurt thin-margin co-ops too.** A board that covers every employer, as Minnesota's does, raises a small co-op's costs like anyone else's, and the California evidence suggests small firms feel that most. Where the work is publicly paid, a floor without higher rates squeezes everyone. The bank's loan reviews test whether a borrower could survive a plausible rise in the floor for its work. The pilot reports failures that follow a new floor separately, while still counting them.
|
||||
- **In the local test, a floor or contract is recorded, not thrown out.** The draw is within states, so a statewide floor lands on both halves alike. Local floors and new union contracts are different: they could come from co-ops' pull, which is the effect itself, or they could be timed to spoil the test. So every market drawn stays in the main result. New local floors and major contracts, defined by a pre-filed threshold for how much of the work they cover, are recorded as outcomes, including whether co-op members helped win them. A version without those markets is only a filed side check.
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **Nationally, it's slow.** Even a clear pass likely means decades before co-ops are common enough to change most workers' options. Nothing here speeds that up. Other policies, like sector-wide wage boards and support for unions, raise pay faster and cost the public less ([Compared with wage boards and unions](#compared-with-wage-boards-and-unions)). This model doesn't replace them.
|
||||
- **The effect is unproven.** The closest evidence cuts both ways: big retailers paying more didn't lift others' pay, and hospitals hiring nurses did. Co-ops might differ from both, because they offer ownership and a vote, not just pay. That's what the local test is for.
|
||||
- **The local test may never run.** On public numbers, even 100 markets could only see an effect of about 5% of pay, bigger than the union evidence suggests co-ops could produce at a tenth of the work. A smaller effect, like the union one for women, would go unseen. Co-ops may never reach the filed share anywhere, and there may not be 100 markets of the right size whose local governments opt in. If the test can't pass its own rule, the honest result is that the labor-market effect stays untested. And the union evidence behind the scale estimate comes from comparing places, not from an experiment.
|
||||
- **Concentrating help will be called unfair.** Conventional employers in the chosen markets will say the government is helping their competitors. The honest answer is that lending terms don't change, the help is time-limited and published, local governments opt in, and it's the only way to learn whether the effect is real. It won't stop the attack.
|
||||
- **Growth can outrun judgment.** Audited demand, capped stages and published results are the defenses. None of them is airtight.
|
||||
- **Success builds a lobby.** If the sector grows, its members will defend the bank, as the electric co-ops defended theirs. That's why growth depends on the published results and a vote each time, not on how many friends the bank has.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: Startups
|
||||
section: Growing the Sector
|
||||
order: 90
|
||||
summary: Fund the science, back the boring, make the co-op a real option for founders, and don't let government pick the startups.
|
||||
---
|
||||
|
||||
# Startups
|
||||
|
||||
**One line: fund the science, backstop the boring, give founders a real co-op option. Don't pick the startups.**
|
||||
|
||||
Government should not be a venture capitalist. VC works by being wrong 70% of the time and letting two freak winners pay for everything. No public agency survives that headline rate. It would get cautious, fund safe bets and well-connected people, and safe bets don't produce breakthroughs. The failure mode isn't corruption; it's risk-aversion under political accountability.
|
||||
|
||||
So:
|
||||
|
||||
1. **Fund the science, not the companies.** DARPA and NIH gave us the internet, GPS, touchscreens, mRNA. The state buys the ingredients; a thousand private actors race to cook.
|
||||
2. **Make the co-op a real option for founders.** A garage company is founders and early employees, with almost no separation between labor and ownership. The bank, internal capital accounts, and the federation exist so that choosing co-op form isn't choosing to be underfunded. Early-stage co-op capital will still cost more than conventional VC. That's [the founder problem](founder-problem.md), and [the test](the-test.md) will show how much it matters.
|
||||
3. **Backstop the boring.** Rural grocery store, local clinic, unglamorous manufacturer. That's development banking, not venture, and government is genuinely good at it. Loan guarantees, SBA-style. These are also the sectors where co-ops have historically done best.
|
||||
@@ -0,0 +1,234 @@
|
||||
---
|
||||
title: How a Locked Co-op Is Taxed
|
||||
section: The Co-op
|
||||
order: 25
|
||||
summary: Federal tax law already lets worker co-ops pay tax on members' profit shares only once, but only if profit is shared by hours worked, so this page fits the model's rules to that law, says what it costs, and lists what the IRS or Congress still needs to confirm.
|
||||
---
|
||||
|
||||
# How a Locked Co-op Is Taxed
|
||||
|
||||
This model relies on an old part of the federal tax code, called Subchapter T, that lets co-ops pass profit to their members and have it taxed only once. Get the details wrong and a co-op pays tax twice on the same profit, which would sink the test for reasons that have nothing to do with whether co-ops work. This page works through those details. It describes federal law as it stands in 2026. It isn't tax advice, and each co-op's bylaws need a tax lawyer's review.
|
||||
|
||||
## How co-ops are taxed today
|
||||
|
||||
An ordinary corporation pays 21% federal tax on its profit ([26 U.S.C. § 11](https://www.law.cornell.edu/uscode/text/26/11)). When it pays what's left to shareholders as dividends, they pay tax again.
|
||||
|
||||
A corporation run as a co-op can skip the first tax on the profit it pays back to the people it does business with, called **patronage dividends** ([§ 1382](https://www.law.cornell.edu/uscode/text/26/1382)). They pay income tax on them instead ([§ 1385](https://www.law.cornell.edu/uscode/text/26/1385)). To count, a patronage dividend has to be ([§ 1388](https://www.law.cornell.edu/uscode/text/26/1388)):
|
||||
|
||||
- **Paid according to how much business each person did with the co-op.** In a worker co-op, the "business" is work, and most US worker co-ops divide profit by hours worked ([Democracy at Work Institute guide](https://institute.coop/sites/default/files/resources/Internal-Capital-Accounts.pdf)). The Tax Court accepted a plywood co-op's split by hours in 1965 (*Puget Sound Plywood v. Commissioner*, 44 T.C. 305), though that case was decided under the law before Subchapter T. Today's practice rests on the IRS accepting it since.
|
||||
- **Owed under rules set before the profit was made,** like bylaws adopted in advance.
|
||||
- **Not taken from other people's share.** A patronage dividend can't include money from the business of people who got nothing, or got less for the same thing ([§ 1388(a)](https://www.law.cornell.edu/uscode/text/26/1388)). The Treasury's example: if a co-op pays nothing to its non-member customers, the part of members' dividends that came from business with them isn't a patronage dividend ([26 C.F.R. § 1.1388-1(a)(2)](https://www.ecfr.gov/current/title-26/chapter-I/subchapter-A/part-1/subject-group-ECFRc609157f1a908c0/section-1.1388-1)). That part is taxed twice.
|
||||
|
||||
Money kept in members' [capital accounts](capital-accounts.md) counts too, if at least 20% of each dividend is paid in cash and the member has agreed to be taxed on the rest now. A bylaw can make membership count as agreement, but only if each member gets a written notice and a copy of the bylaw ([§ 1388(c)](https://www.law.cornell.edu/uscode/text/26/1388)). With automatic membership ([When People Don't Stay Long](high-turnover.md)), that notice is handed over, and signed for, when someone joins. Everything has to be paid or credited within eight and a half months after the year ends.
|
||||
|
||||
**The reserve is taxed.** Profit the co-op keeps for itself isn't paid to anyone, so the co-op pays the 21% on it. US worker co-ops that keep a permanent reserve already work this way ([Democracy at Work Institute guide](https://institute.coop/sites/default/files/resources/Internal-Capital-Accounts.pdf)).
|
||||
|
||||
**In the standard example** from [Member Capital Accounts](capital-accounts.md#where-each-years-profit-goes), a co-op makes $600,000. The $360,000 paid to members is taxed once, on their own returns. The $180,000 for the reserve is taxed at 21%, about $37,800, so about $142,000 is actually saved. If the $60,000 paid to the federation fund can't be deducted, the tax is about $50,400. Whether it can depends on how the federation is set up (see below).
|
||||
|
||||
## Why a locked co-op is a corporation
|
||||
|
||||
Many small businesses are partnerships, or LLCs taxed as partnerships, which pay no business-level tax. That doesn't work here, for two reasons:
|
||||
|
||||
- **Partners can't be employees.** The IRS treats partners as self-employed, not as employees of their own partnership ([IRS, Publication 541](https://www.irs.gov/publications/p541); [IRS practice unit](https://www.irs.gov/pub/fatca/int_practice_units/self-employment-tax-partners.pdf)). [The pilot](pilot-spec.md#who-can-borrow) requires members to be on the payroll, so they have minimum wage and workers' compensation, and unemployment insurance in most states ([Owners Are Still Workers](labor-law.md)).
|
||||
- **Partners are taxed on all the profit, including what's kept.** Members would owe tax each year on their share of a reserve they can never take out.
|
||||
|
||||
So a locked co-op is a corporation, or an LLC that chooses to be taxed as one ([26 C.F.R. § 301.7701-3](https://www.law.cornell.edu/cfr/text/26/301.7701-3)), and files the co-op version of the corporate tax return.
|
||||
|
||||
## Does a locked co-op count as a co-op?
|
||||
|
||||
Subchapter T applies only to a corporation "operating on a cooperative basis" ([§ 1381](https://www.law.cornell.edu/uscode/text/26/1381)). Courts and the IRS read that as three things: members control it democratically, capital doesn't control it or take its profits, and profit goes back to the people who did business with it, according to how much they did. If a co-op fails this test, it loses Subchapter T entirely, not just one dividend.
|
||||
|
||||
Two features of this model could strain the test:
|
||||
|
||||
- **The lock-holder.** An outside organization holds a share that can veto a sale ([The Lock](the-lock.md)). But that share carries no profit and no say in running the business. It only blocks the co-op from being sold.
|
||||
- **The reserve goes to the federation, not the members, if the co-op closes.** Some US worker co-ops already write this into their bylaws, sending leftover assets to groups that support co-ops ([Democracy at Work Institute guide](https://institute.coop/sites/default/files/resources/Internal-Capital-Accounts.pdf)). But no ruling covers it for this model's co-ops.
|
||||
|
||||
This is the first question for the IRS, and for Congress (see [Getting certainty](#getting-certainty)).
|
||||
|
||||
## The model's rules, fitted to the law
|
||||
|
||||
### What counts as an hour
|
||||
|
||||
Profit shares follow hours, so hours have to be defined in advance, in the bylaws:
|
||||
|
||||
- **Hours paid**: hours worked, as wage law counts them, plus paid leave and state paid family leave.
|
||||
- **Salaried staff count 40 hours a week.** Overtime counts once, not time and a half. No one counts more than 2,080 hours a year.
|
||||
- **Only hours on the payroll records** that match the co-op's quarterly payroll tax filings. Time on committees and the board counts only up to a small share of a person's hours. The co-op's auditor checks a sample of timesheets each year.
|
||||
|
||||
Unpaid leave, even protected leave, isn't paid hours. Crediting hours nobody worked could mean paying one person out of another's work, which the rule above forbids. So a co-op that wants leave to count has to offer paid leave. Time on leave still counts toward the ramp, as [Worker Ownership](worker-ownership.md) promises.
|
||||
|
||||
### Wages first
|
||||
|
||||
Before any profit is shared, every worker is paid at least the going rate for their job for their hours ([Investing for the Long Run](the-long-run.md#the-rules)). If the year's wages fell short, the shortfall is paid first, as wages.
|
||||
|
||||
Why: members are employees. If a co-op paid itself low wages and large profit shares, the IRS could say the profit shares are really wages in disguise, and owe payroll tax (Social Security and Medicare) and penalties. That's the same argument the IRS uses against small-business owners who pay themselves too little salary. Paying the going rate first makes the profit share clearly profit. It also helps low-paid members: wages count toward the Earned Income Tax Credit and Social Security, and profit shares don't.
|
||||
|
||||
### One rate per hour, for everyone who works
|
||||
|
||||
The members' profit pool is divided by hours worked. Before the ramp, every hour earns the same amount, and "everyone's share is identical" means identical **per hour**. A part-timer who got the same check as a full-timer would be collecting from the full-timer's work.
|
||||
|
||||
**Non-members get a share too.** The tax law's word is "patron," not "member," so people on probation, and people who chose not to join, can receive patronage dividends. In this model they get the same share per hour as a member starting the ramp, all in cash. The rest of what their hours earned goes to the reserve, on top of the 30% minimum. The reserve still benefits the co-op, which members govern, but no member's own check comes from a non-member's work. Hiring non-members can't raise members' profit shares.
|
||||
|
||||
### The tenure ramp
|
||||
|
||||
The founding plan has a new member start at a partial profit share and reach a full one over five to ten years ([Worker Ownership](worker-ownership.md)). As first written, the part a new member didn't get went to the other members. The tax law doesn't allow that: a new member and a ten-year member who work the same hours are doing the same business with the co-op, so the ten-year member can't be paid out of the new member's work without that part being taxed twice.
|
||||
|
||||
**So the ramp's unpaid part goes to the reserve.** A member at half a share gets half the per-hour amount. The other half goes to the reserve, on top of the 30% minimum.
|
||||
|
||||
**What that costs, in the standard example.** Say the 40 members are 32 at a full share and 8 new members at half a share, all working the same hours. As first written, full members got $10,000 and new members $5,000. Now the $360,000 pool is $9,000 for each member's hours. Full members get $9,000. New members get $4,500, and the other $4,500 of each, $36,000 in all, goes to the reserve, which gets $216,000 instead of $180,000. If new members work fewer hours than the others, the full members lose less. In a co-op where few members are new, they lose little.
|
||||
|
||||
That changes what the ramp does. The founding plan's reason for it was to stop every new hire from cutting into everyone else's share. Now it doesn't do that: a new member's hours count in dividing the pool from the start. What the ramp still does is make new members earn their way to a full share, with the difference building up the co-op's own reserve instead of senior members' checks.
|
||||
|
||||
**Two other ways, both with costs.** A co-op can choose either in its bylaws, before the year starts:
|
||||
|
||||
- **Pay the ramp as a wage for years of service.** Everyone gets the per-hour profit share at the starting rate. Senior members get the rest as a pay supplement for years of service, which employers can pay and deduct. That keeps the founding plan's shield for senior members, but it costs payroll tax, about 15% split between the co-op and the member, on most of the profit share. And wages can't be held in capital accounts, so the accounts, and the money the co-op can reinvest from them, shrink.
|
||||
- **Ask the IRS to accept the old result.** The unpaid part could count toward the reserve's 30% minimum, which lets the co-op put that much more into the members' pool, so senior members get what they did before. But then senior members gain exactly what new members give up, and the IRS could fairly say it's the old formula by another route. It's only an option if the IRS rules that it's allowed.
|
||||
|
||||
### The founding share
|
||||
|
||||
[The Founder Problem](founder-problem.md#the-tools) sets aside 10% of the members' pool for the first ten years for the people who worked there in the first three years. Paid as a profit share, it would likely fail the patronage test. The rules exclude amounts paid out of other patrons' earnings to people doing "substantially identical transactions" ([Treas. Reg. § 1.1388-1(a)(2)(ii)](https://www.law.cornell.edu/cfr/text/26/1.1388-1)), and a founder and a later hire working the same hours in the same year are doing much the same business with the co-op. Some worker co-ops measure patronage by wages instead of hours, which could change that. This model uses hours ([One rate per hour](#one-rate-per-hour-for-everyone-who-works)).
|
||||
|
||||
**So it's paid as pay,** in the year it's earned, and only to people still working there that year.
|
||||
|
||||
- **Deferred-pay rules.** Pay that vests by working the year and is paid soon after that year ends is generally not "deferred" under the rules for deferred pay ([26 C.F.R. § 1.409A-1(b)(4)](https://www.law.cornell.edu/cfr/text/26/1.409A-1)). Paying it only to people still there, each year, is meant to fit that. The standard bylaws still need a tax lawyer's review.
|
||||
- **Reasonable pay.** It's pay set as a share of profit, and the IRS can treat unreasonable pay to owners as a disguised dividend ([26 U.S.C. § 162(a)(1)](https://www.law.cornell.edu/uscode/text/26/162)). The cap, no more than the going-rate pay for the person's job each year, keeps a founder's total at most about double the going rate. The first pilot co-ops include it in their ruling request (below).
|
||||
- **Overtime.** For hourly workers it's a bonus they're owed, so it counts in their overtime rate for the year it's paid.
|
||||
- **It costs payroll tax,** paid by the co-op and the person, and it's paid in cash, since wages can't be held in capital accounts.
|
||||
|
||||
### Capital accounts
|
||||
|
||||
- **The usual way.** Members agree to be taxed now on what's credited to their accounts, and at least 20% is paid in cash. The model's default is half in cash, which covers the tax for most members.
|
||||
- **Members with lower incomes can choose not to be taxed until they're paid.** The law allows a second kind of credit, a "nonqualified" notice. The co-op pays the 21% now and gets it back as a deduction when the account is paid out, and the member pays tax only when they receive the money ([§ 1382(b)(2)](https://www.law.cornell.edu/uscode/text/26/1382)). That will be the default for members under twice the poverty line who take the standard split. It waits for the Labor Department's answer on pension law, because an untaxed credit a member chooses in advance looks most like the deferred-pay plans courts have called pension plans ([Member Capital Accounts](capital-accounts.md#why-not-pay-it-out-when-you-leave)). Until then, those members take their whole share in cash by default, or the standard split, taxed now. These credits follow extra timing rules, [below](#credits-taxed-later-the-deferred-pay-rules).
|
||||
- **Charges come out of this year's share first.** Money members already paid tax on can be moved back to the reserve, under [the pay rule](the-long-run.md#the-rules) or when losses are charged to accounts. That's probably taxable to the co-op a second time, and the member may be able to deduct only part of it. So those charges come out of the current year's profit share first, before it's credited or taxed, and reach older balances only when that isn't enough.
|
||||
|
||||
### Interest on accounts
|
||||
|
||||
Accounts earn a return by formula ([Member Capital Accounts](capital-accounts.md#how-the-account-grows)): the one-year Treasury rate plus two points while a member works there, the plain Treasury rate after they leave. How that's taxed depends on whether an account is a debt the co-op owes or an owner's stake in it.
|
||||
|
||||
- **A business deducts interest only on a debt** ([26 U.S.C. § 163(a)](https://www.law.cornell.edu/uscode/text/26/163)). The factors the tax code lists for telling a debt from an owner's stake include an unconditional promise to pay a fixed sum on a set date at a fixed rate, and whether the money ranks behind the business's other debts ([§ 385(b)](https://www.law.cornell.edu/uscode/text/26/385)).
|
||||
- **An account is an owner's stake.** It shares in losses, ranks behind lenders, and its payouts can be paused. So its return is probably a return on capital, not deductible interest, even after the member leaves. Co-ops can deduct dividends on capital only if they're tax-exempt farm co-ops ([§ 1382(c)(1)](https://www.law.cornell.edu/uscode/text/26/1382), [§ 1381(a)](https://www.law.cornell.edu/uscode/text/26/1381)). For everyone else, the Treasury's rule is that the profit split by patronage is what's left *after* dividends on "capital stock or other proprietary capital interests" ([26 C.F.R. § 1.1388-1(a)(1)](https://www.law.cornell.edu/cfr/text/26/1.1388-1)). So the co-op pays its 21% on the return first.
|
||||
- **The return is paid every year, not just in profit years.** The model used to pay the extra two points only when the co-op made a profit. That made it look like capital taking a share of the profit, which is one of the things the test for being a co-op at all rules out ([above](#does-a-locked-co-op-count-as-a-co-op)). A fixed, capped rate paid in good years and bad is how co-ops have long paid capital. It's credited to the account, not paid in cash, so a loss year doesn't cost the co-op cash.
|
||||
- **Account holders pay the tax on their own return.** The return is credited after the co-op's 21% tax on it. With the Treasury rate at 4%, a current member's 6% is credited as about 4.7%, and a former member's 4% as about 3.2%. Otherwise members who take all their share in cash, as lower-income members can, would pay part of the tax on other people's savings.
|
||||
- **The return itself comes off the top.** Because it's paid before the patronage split, the reserve, the federation fund and everyone's per-hour share all carry it. In the standard example, a co-op making $600,000 credits at most $180,000 a year to accounts, so after five years at most about $900,000 is waiting to be paid out. At 6%, that's at most about $54,000 a year, less in practice, since newer members, all-cash members and the insured slice (which earns the plain rate) hold less. A member with no account carries at most about $800 a year of it. That's the price of the capital the whole co-op uses instead of borrowing it, and it's published every year, per hour worked and per dollar in accounts.
|
||||
- **Members are probably taxed when they're paid, not when it's credited.** The money can't be touched until then. When it's paid, it's probably a dividend. A worker co-op isn't a tax-exempt corporation, so it may count as a "qualified" dividend, taxed at the lower rates for investment income, which are zero for many lower-paid members ([§ 1(h)(11)(B)](https://www.law.cornell.edu/uscode/text/26/1)). So the second tax is often small. But crediting some members' accounts while others take cash could be treated as a payment when it's credited ([§ 305(b)(2), (c)](https://www.law.cornell.edu/uscode/text/26/305)). That goes on the list for the IRS ([below](#getting-certainty)).
|
||||
- **If the IRS says the return breaks the co-op test,** the premium drops to the plain Treasury rate automatically, under the bylaws, for returns not yet credited.
|
||||
- **Why not make a former member's balance a debt?** It looks like a way to make their interest deductible, but it doesn't hold up. Interest that can be paused isn't "unconditionally payable," so it would be taxed each year before it's paid anyway ([26 C.F.R. § 1.1273-1(c)](https://www.law.cornell.edu/cfr/text/26/1.1273-1); [§ 1272(a)](https://www.law.cornell.edu/uscode/text/26/1272)). A note running several years must pay at least the federal rate for its term, which can be above the one-year rate ([§ 1274(d)](https://www.law.cornell.edu/uscode/text/26/1274)). Interest that can't be paused would pay people who left ahead of those who stayed, which the pause rules forbid. And a balance that turns loss-proof the day someone quits rewards quitting just before a bad year. The deduction isn't worth that.
|
||||
|
||||
### Credits taxed later: the deferred-pay rules
|
||||
|
||||
A nonqualified credit isn't taxed until it's paid, years after the work. Federal tax law has strict rules for pay that's put off like that ([26 U.S.C. § 409A](https://www.law.cornell.edu/uscode/text/26/409A)). They cover only pay for services, and the model's position is that a profit share is an owner's share, not pay ([Owners Are Still Workers](labor-law.md#is-the-profit-share-pay)). Nobody has decided whether they reach a co-op's patronage credits. So the model follows them as a precaution, and the bylaws say it does so without conceding that they apply. Breaking them would be expensive for exactly the members this option exists to help. Everything put off becomes taxable at once, with interest and an extra 20% tax (§ 409A(a)(1)(B)). So once nonqualified credits are offered:
|
||||
|
||||
- **Only co-ops using the federation's software offer them.** The rules below are easy to get wrong by hand. In other co-ops, lower-income members choose between all cash and the standard split, taxed now.
|
||||
- **Only lower-income members get them, by a choice made before the year starts.** A member chooses between the standard split and all cash by the end of the year before, and the choice holds for the whole year ([26 C.F.R. § 1.409A-2(a)(3)](https://www.law.cornell.edu/cfr/text/26/1.409A-2)). A new member can choose within 30 days of joining, and then it covers only profit from hours worked after the choice (§ 1.409A-2(a)(7)). The same choice fixes how the credit will be paid: at once, or in monthly payments that all fall in one calendar year. Whether a member is under twice the poverty line is judged on the higher of the last two years' household income, from their tax returns, so one low year doesn't qualify. The co-op can't decide after a year ends which credits are nonqualified.
|
||||
- **The schedule is fixed before the year starts.** A vote to change the wait applies to nonqualified credits only for profit from years that start after the vote.
|
||||
- **They're paid unless paying would put the co-op at risk.** Deferred pay can be held back past its date only if paying it would threaten the employer's ability to keep going, and then it has to be paid in the first year it wouldn't ([§ 1.409A-3(d)](https://www.law.cornell.edu/cfr/text/26/1.409A-3)). That's narrower than the co-op's ordinary solvency test, which also pauses payouts that would break its loan terms or its minimum cash level. So by December 1 each year, the co-op's outside accountant looks at the last checked accounts and a forecast, and says in writing whether paying the nonqualified credits due would have that effect. If not, they're paid by December 31. The bank's loan terms leave these payments out of its tests, so paying them doesn't put a co-op in default, and the federation pays for the finding.
|
||||
- **No five-year note.** After a two-year pause, other paused payouts become a debt repaid over five years ([Member Capital Accounts](capital-accounts.md#protecting-the-co-op-and-the-people-who-stay)). Stretching nonqualified credits out that way would count as putting them off, which the rules allow only with a year's notice and at least five more years' wait (§ 1.409A-2(b)(1)). So they stay paused and are paid in full in the first year the co-op can pay them without that risk. That means they can be paid ahead of former members waiting on the note, an exception to the rule that the same year's credits are treated the same. They belong to the lowest-paid members, and are usually small.
|
||||
- **Small balances follow a rule set in advance.** Paying a whole balance early is allowed if it's under a federal limit well above the $2,000 used here, pays out everything the co-op owes that person under every similar deferred arrangement, and the co-op's decision is in writing by the payment date (§ 1.409A-3(j)(4)(v)). So the plan counts all of a person's nonqualified credits together, and the board records the decision at each payout.
|
||||
- **If a co-op gets it wrong, it fixes it and pays the cost.** It first uses the IRS's correction program, which cuts or removes the extra tax for mistakes fixed quickly ([IRS Notice 2008-113](https://www.irs.gov/pub/irs-drop/n-08-113.pdf)). Whatever extra tax and interest remain, the co-op pays, grossed up for the tax on that payment. That payment is wages, so it goes through payroll, and it's paid by the end of the year after the member pays the tax (§ 1.409A-3(i)(1)(v)). It doesn't make up for public benefits a member loses when years of income land at once.
|
||||
|
||||
### If the co-op fails
|
||||
|
||||
Members pay ordinary income tax on account credits in the year they're made. If the co-op fails and the account is lost, the loss is probably a capital loss, which is worth much less. A credit written as stock, or as a registered certificate of what the co-op owes, is a "security" for this purpose, and a worthless security counts as a capital loss ([§ 165(g)](https://www.law.cornell.edu/uscode/text/26/165)). A credit that's neither is probably a personal debt gone bad, also a capital loss ([§ 166(d)](https://www.law.cornell.edu/uscode/text/26/166)). A member could argue the credit was tied to their job, which the law treats as a business debt. But for an employee that's a miscellaneous itemized deduction, and those are suspended for good ([§ 67(h)](https://www.law.cornell.edu/uscode/text/26/67)), so that route is worth nothing. Capital losses beyond any capital gains offset only $3,000 a year of other income, and the rest carries forward ([§ 1211(b)](https://www.law.cornell.edu/uscode/text/26/1211)). Some states are harsher.
|
||||
|
||||
So a member who loses $20,000 deducts $3,000 a year for seven years, unless they have capital gains to set it against. For a low-paid member who owes little income tax, the deduction is worth little. They were taxed in full on money they couldn't take out, and get little back when it's lost.
|
||||
|
||||
The small-business stock rule, which lets owners take up to $50,000 a year of such a loss against any income ([§ 1244](https://www.law.cornell.edu/uscode/text/26/1244)), probably doesn't help. It covers only stock issued for money or property, and Treasury's rules say stock issued for services doesn't count ([26 C.F.R. § 1.1244(c)-1(d)](https://www.law.cornell.edu/cfr/text/26/1.1244(c)-1)). Credits are figured by hours worked. It also stops applying once a company has taken in $1 million for its stock, which a mid-sized co-op passes in a few years. And it helps most the members who pay the most tax.
|
||||
|
||||
**Less exposure first.** The model's main answer is to keep less taxed money at risk:
|
||||
|
||||
- **Losses hit this year's share first.** Charges come out of the current year's profit share, which hasn't been taxed yet, before they reach older balances ([above](#capital-accounts)). That part of a loss costs members nothing in tax.
|
||||
- **Accounts are capped** at one year's pay, and at least a fifth of each year's share, half by default, is paid in cash.
|
||||
- **The insurance pool** covers former members' insured amounts and a protected part of current accounts ([Protecting Members' Savings](savings-guarantee.md)).
|
||||
- **Once the Labor Department answers** on pension law, credits taxed only when paid could be offered more widely. A credit that was never taxed can't be lost after tax.
|
||||
|
||||
**Then a general fix.** The law the bank needs anyway also gives back the tax a worker actually paid on money they had no right to take out, when their employer's failure wipes it out. It's figured from the worker's own past returns and refundable, so it's worth the same to a low-paid member as a high-paid one. It isn't only for co-ops: any worker taxed on pay their employer held back and lost gets the same. It applies only to failures under a court's supervision. Credits made in the last year before the failure count only if the co-op's outside accountant has already checked that year's books.
|
||||
|
||||
**Timing.**
|
||||
|
||||
- **The pool comes first.** While a claim on the pool is pending, the loss usually can't be claimed yet. A member claims whatever the pool doesn't cover once it has decided. If the pool pays after a member has already deducted the loss, that payment is taxable to the extent the deduction saved them tax ([§ 111](https://www.law.cornell.edu/uscode/text/26/111)).
|
||||
- **Members have seven years.** Claims for refunds based on worthless securities or bad debts can be made up to seven years after the return was due ([§ 6511(d)(1)](https://www.law.cornell.edu/uscode/text/26/6511)), because the year a loss happens is often unclear.
|
||||
- **Members get the facts in writing.** The trustee or the pool sends each person their account balance, what they got back and when, and the key dates in the case. It can't decide the tax year for them. The co-op's [benefits check](#what-the-co-op-and-federation-do) helps them file.
|
||||
- **Losses charged while the co-op is running** can't be deducted when they're charged. They show up only when the account is finally paid out for less than was taxed, probably as a capital loss.
|
||||
|
||||
Credits taxed only when paid were never taxed, so there's no loss to claim on them.
|
||||
|
||||
## Payroll tax on profit shares
|
||||
|
||||
Wages carry payroll tax, about 15% in all, split between the co-op and the worker. Whether patronage dividends paid to worker-members do too is unsettled. The likelier challenge is that profit divided by hours worked is really wages. A second theory is that it's self-employment income. The IRS has claimed the second against members of at least one worker co-op, then backed down when challenged, without a published explanation ([Gregory Wilson, co-op tax lawyer](http://www.gwilson.com/documents/Article4(PatronageDivTax).pdf)). His argument: a member paid a fair wage through payroll is like a shareholder who works for the company, and a shareholder's dividends don't carry payroll tax. The wages-first rule is built to support that argument, and the model makes the same argument under state wage laws, though not every law sees it the same way ([Owners Are Still Workers](labor-law.md#is-the-profit-share-pay)).
|
||||
|
||||
If the IRS wins, members pay about 15% more on their profit shares. Low-paid members would at least earn Social Security credit and Earned Income Tax Credit on it.
|
||||
|
||||
## Benefits
|
||||
|
||||
Many care and cleaning workers count on public programs, and becoming a member shouldn't cost anyone their benefits. The aim is for benefits agencies to count this money correctly, not to hide it from them. Programs look at two things: savings and income.
|
||||
|
||||
### Savings
|
||||
|
||||
Some programs have a savings limit. For Supplemental Security Income (SSI), it's $2,000 for one person ([20 CFR 416.1205](https://www.law.cornell.edu/cfr/text/20/416.1205)), and Medicaid for older and disabled people generally uses similar rules. Medicaid for most working-age adults has no savings limit at all: "A State shall not apply any assets or resources test" ([42 U.S.C. 1396a(e)(14)(C)](https://www.law.cornell.edu/uscode/text/42/1396a)).
|
||||
|
||||
**The membership share and credits that haven't come due shouldn't count as savings,** because members can't turn them into cash. SSI counts property a person "could convert to cash," and "if a property right cannot be liquidated, the property will not be considered a resource" ([20 CFR 416.1201](https://www.law.cornell.edu/cfr/text/20/416.1201)). Food assistance (SNAP) doesn't count "resources having a cash value which is not accessible to the household" either ([7 CFR 273.8(e)(8)](https://www.law.cornell.edu/cfr/text/7/273.8)). That only holds if members really can't get at the money. So the standard bylaws say that **the share and account credits can't be withdrawn early, sold, given away, pledged or borrowed against**, except that they pass to a named person on death and a court can divide them in a divorce ([Member Capital Accounts](capital-accounts.md#how-its-paid-out)). The co-op gives members a statement of the facts for their caseworker: the balance, the payment dates and that clause.
|
||||
|
||||
### Income
|
||||
|
||||
Profit shares count as income for Medicaid, marketplace health insurance subsidies, food assistance and housing aid. Four things make this harder than it looks.
|
||||
|
||||
**SSI may count a credit when it's made, not when it's paid.** SSI counts income "at the earliest of the following points: when you receive it or when it is credited to your account or set aside for your use" ([20 CFR 416.1123](https://www.law.cornell.edu/cfr/text/20/416.1123); the same for wages in [§ 416.1111](https://www.law.cornell.edu/cfr/text/20/416.1111)). A member under twice the poverty line can already choose to take their whole share in cash, so a credit they leave in the account could be seen as income they set aside. So **members on SSI get their whole share in cash, with nothing credited to an account and nothing held back for the buy-in,** until Social Security says how it counts credits. They're full members all the same.
|
||||
|
||||
**SSI may count the share as the less favorable kind of income.** Wages, including "bonuses... received because of your employment," are earned income ([20 CFR 416.1110](https://www.law.cornell.edu/cfr/text/20/416.1110)), and SSI ignores the first $65 a month and half of the rest ([§ 416.1112](https://www.law.cornell.edu/cfr/text/20/416.1112)). Dividends, "returns on capital investments," are unearned income ([§ 416.1121](https://www.law.cornell.edu/cfr/text/20/416.1121)), and SSI ignores much less of it. The model argues the share is an owner's share for tax purposes ([Owners Are Still Workers](labor-law.md#is-the-profit-share-pay)), and Social Security may take it at its word. So every example shown to a member on SSI assumes the worse answer, and the same question matters for other rules for disabled workers, including the ones that let them keep Medicaid as they earn more. Opting out of membership doesn't change this, because workers who aren't members get a cash share too.
|
||||
|
||||
**Timing helps some programs and hurts others.** SNAP treats a one-time lump sum as savings, not income ([7 CFR 273.9(c)(8)](https://www.law.cornell.edu/cfr/text/7/273.9)). SSI counts it as income in the month it arrives. So a payout from an account can come as one payment or up to 12 monthly payments, and which is better depends on the member's programs. The federation's benefits advice helps each member choose.
|
||||
|
||||
**ABLE accounts protect savings, not income.** A member with an ABLE account, a savings account for people whose disability began young, can have payouts paid into it. Balances up to $100,000 don't count against SSI's savings limit, but the money still counts as income in the month it's paid, and deposits are capped each year ($20,000 in 2026) ([POMS SI 01130.740](https://secure.ssa.gov/poms.nsf/lnx/0501130740)).
|
||||
|
||||
### What the co-op and federation do
|
||||
|
||||
- **A benefits check before anyone is signed up automatically.** The federation, not the co-op, offers every worker on probation a confidential benefits check in their own language. The co-op doesn't ask anyone about disability or benefits. A worker the check shows is on SSI or disability benefits becomes a member only if they say yes.
|
||||
- **A statement at every credit and every payment,** so members can report both to their agencies on time. Members are told they must report them.
|
||||
- **Getting an answer from Social Security.** Social Security doesn't give advance rulings. The federation asks it in writing for guidance on how it counts profit shares and credits, and supports a real member's case through appeal if needed. It shares the answer with every co-op.
|
||||
- **Benefits advice for every member,** along with the templates and training the federation already supplies ([The Federation](federation.md)).
|
||||
|
||||
The model doesn't shrink the benefit cliffs themselves. A low-paid member on several programs at once can lose much of a profit share to lower benefits and higher rent. The advice can show them that before they decide. It can't change it.
|
||||
|
||||
## Getting certainty
|
||||
|
||||
The IRS gives binding answers on specific facts, called private letter rulings, but only to the taxpayer who asks, only about its own real transactions, and never as precedent for anyone else ([26 U.S.C. § 6110(k)(3)](https://www.law.cornell.edu/uscode/text/26/6110)). A ruling costs a fee and often takes months, and the IRS can decline to give one. So:
|
||||
|
||||
- **The first pilot co-ops ask, and the bank pays.** They ask whether the co-op operates on a cooperative basis with the lock and the reserve, whether the per-hour profit shares and the ramp count as patronage dividends, whether the founding share is deductible as pay, whether profit shares carry payroll tax when wages meet the going rate, how charges moved back to the reserve are taxed, whether the return on accounts strains the cooperative test and when members are taxed on it, and whether the deferred-pay rules, or the payroll-tax rule for deferred pay ([26 U.S.C. § 3121(v)(2)](https://www.law.cornell.edu/uscode/text/26/3121)), apply to nonqualified credits at all.
|
||||
- **Until then, every co-op uses the rules on this page, and gets a tax lawyer's opinion on its bylaws.** None of them depends on a ruling to work.
|
||||
- **Congress settles it.** The bank's own law needs Congress anyway, and so does [the bankruptcy amendment](bankruptcy.md). That law also says a locked co-op that meets these rules operates on a cooperative basis, and that its profit shares aren't wages for members paid at least the going rate. That part is a statement of how existing law applies, not a lower tax rate, though settling the payroll-tax question is worth real money to members ([How this compares](#how-this-compares-with-other-businesses)). The same law also makes one real change, open to any employer's workers: it refunds the tax they paid on money they lose when their employer fails ([If the co-op fails](#if-the-co-op-fails)).
|
||||
|
||||
## How this compares with other businesses
|
||||
|
||||
A co-op isn't taxed more lightly than other small businesses across the board, or more heavily. It depends on what it's compared with, and on how the payroll-tax question comes out ([above](#payroll-tax-on-profit-shares)).
|
||||
|
||||
**Profit paid to members** is taxed once, at their own rates. So is the profit of partnerships, sole owners and S corporations (small corporations whose owners are taxed on the business's profit as if it were their own). But those owners can usually deduct up to 20% of their business profit first ([26 U.S.C. § 199A](https://www.law.cornell.edu/uscode/text/26/199A)), a deduction the 2025 tax law made permanent. Co-op members can't. So:
|
||||
|
||||
- **Against a partnership or a sole owner, the co-op comes out ahead, if its payroll-tax position holds.** Those owners pay self-employment tax, about 15%, on their whole profit. If a member's profit share carries no payroll tax, as the model argues, a $9,000 share avoids about $1,270 of it, against a deduction worth $200 to $400 at the rates most members pay.
|
||||
- **Against an S corporation, the co-op is somewhat behind.** An S corporation owner who works in the business takes a reasonable salary, which carries payroll tax and gets no deduction, and takes the rest as profit with no payroll tax. That's the closest match to the model's wages-first rule. The difference is the deduction members don't get.
|
||||
- **If the IRS wins on payroll tax, the co-op is behind everyone.** Members would pay payroll tax on their profit shares and still get no deduction. For low-paid members, the loss is partly made up: a profit share that counts as earnings earns Social Security credit and Earned Income Tax Credit, which in its lowest range adds 34 to 45 cents for each extra dollar earned by a worker with children. For them, winning the payroll-tax argument isn't clearly better.
|
||||
|
||||
**Why members can't take the deduction.** It covers profit from a business the taxpayer carries on themselves (§ 199A(c)(1), (d)). Working as someone's employee isn't one (§ 199A(d)(1)(B); [26 C.F.R. § 1.199A-5(d)](https://www.law.cornell.edu/cfr/text/26/1.199A-5)), and owning a share of a corporation isn't either. Patronage dividends count only when the patron does business with the co-op as part of a business of their own, like a farmer selling grain through a marketing co-op ([§ 199A(c)(3)(B)(ii)](https://www.law.cornell.edu/uscode/text/26/199A); [26 C.F.R. § 1.199A-7](https://www.law.cornell.edu/cfr/text/26/1.199A-7)). A worker-member has no business of their own for the dividend to belong to.
|
||||
|
||||
That's the same position the model takes on payroll tax: a profit share is an owner's return on a corporation, not pay and not business income. A member can't have it both ways. Claiming the deduction would mean saying the share is business income, which is the IRS's self-employment theory. So:
|
||||
|
||||
- **Members don't claim it.** The co-op's year-end patronage forms report nothing for it, and the tax lawyer's opinion on the bylaws covers it.
|
||||
- **Members join only as individuals on the payroll,** not through companies of their own. Setting up as contractors wouldn't work anyway: the rules presume someone doing the same work for a former employer is still an employee (§ 1.199A-5(d)(3)).
|
||||
- **The co-op itself can't take it,** because it's a corporation. The only co-ops with a deduction of their own are farm and garden co-ops (§ 199A(g)). The new $400 minimum deduction (§ 199A(i)) doesn't reach members either.
|
||||
|
||||
**What it's worth.** In the standard example, members share $360,000 of profit. The deduction on that would be worth roughly $9,000 to $16,000 a year across 40 members. It's close to nothing at the bottom, since it's capped at 20% of taxable income and many low-paid members owe little. In a conventional firm, the same profit would go to one owner, whose deduction would be worth about $23,000 to $25,000 a year, and an S corporation owner can deduct on profit kept in the business too. The gap is mostly federal: most states start from a figure that comes before this deduction.
|
||||
|
||||
**The model doesn't ask Congress to extend it.** The deduction is a break for business owners that's worth most to the best-paid. Asking for a share of it would make the pilot a subsidized test. The model does ask Congress to say that profit shares aren't wages for members paid the going rate ([Getting certainty](#getting-certainty)). That isn't nothing: it's worth more than the deduction. It's the same treatment an S corporation owner gets on profit above a reasonable salary, and it's why the going-rate test has to be strict.
|
||||
|
||||
**Profit the co-op keeps** is taxed at 21% and never again, because nobody can ever take it out. A conventional corporation's kept profit is taxed at 21% and again when it's paid out. A partnership's or S corporation's owners are taxed on it at their own rates, which can be higher or lower, and they own it.
|
||||
|
||||
**The difference that matters:** a conventional owner's kept profit is still theirs. The co-op's reserve belongs to no one, and it still pays tax. That's a cost of the lock, and this model doesn't ask for a tax break to cover it.
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **The ramp no longer shields senior members.** Under tax law, the founding plan's reason for the ramp works only through the reserve, through payroll tax, or with the IRS's blessing. Which is best is open.
|
||||
- **Workers without work authorization can't be members.** Being on the payroll requires it. Some immigrant cleaning co-ops today are partnerships partly so members with only a taxpayer number can be owners. This model can't do that without giving up payroll protections, and it leaves those workers out.
|
||||
- **Unpaid leave costs profit share.** Mostly for women, unless the co-op pays for leave.
|
||||
- **States differ.** Most states follow the federal treatment of co-ops. But some tax a business's gross receipts, like Ohio and Washington, where the patronage deduction doesn't help. And some states' co-op laws may send leftover assets to members when a co-op closes, which would clash with the lock. Each pilot state needs checking.
|
||||
- **The federation's form matters.** If the federation fund is a charity, a co-op's payments and a closing co-op's reserve are gifts, and corporations can deduct gifts only up to a limit ([§ 170(b)(2)](https://www.law.cornell.edu/uscode/text/26/170)). If it's a trade association, payments may be deductible as a business expense. The choice isn't made.
|
||||
- **The return on accounts isn't deductible.** It's most likely a return on capital, so the co-op pays 21% on it, charged to the accounts that earn it, and members may pay tax again when they're paid ([Interest on accounts](#interest-on-accounts)).
|
||||
- **It's a lot of paperwork.** A co-op tax return, a patronage form for every worker, written notices, consent records, hours tracking, and the ramp. The federation provides standard software and filing help at cost, and the pilot's business help pays for setting it up.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Making the Case
|
||||
section: Making the Case
|
||||
order: 150
|
||||
summary: Lead with "nobody is forced," point to the co-op banks that already exist, argue from freedom rather than dollars, and don't overclaim.
|
||||
---
|
||||
|
||||
# Making the Case
|
||||
|
||||
**Lead with "nobody is forced."** The model takes nothing from anyone. It charters a bank, asks it to lend at market rates to a kind of firm other banks won't touch, and publishes the results. If co-ops can't compete, the experiment says so and it gets wound down. That is the most market-friendly proposal on the left, and it should be argued that way.
|
||||
|
||||
**The second line: "we already did this."** Congress chartered a cooperative bank in 1978. Mondragon built one in the 1950s. Italy has run a co-op development fund on member profits for decades. This is an old, tested idea aimed at a new target. The record, failures included, is in [Has This Been Tried?](precedents.md).
|
||||
|
||||
**Germany is now a supporting point, not the lead.** Co-determination puts workers on up to half the supervisory board of large firms, by law, and Germany is still a capitalist market economy. It shows ownership and governance rules are choices societies make, not laws of nature. But it's no longer needed to defend a mandate, because there isn't one.
|
||||
|
||||
**Don't lead with "they extract while workers starve."** It's the weakest version of the case, and it invites "the return is compensation for capital and risk," which wins the exchange. The strong version:
|
||||
|
||||
> *Bargaining power is lopsided. Workers have no realistic path to a claim on the enterprise they build. Capital markets don't fund firms that workers own. Fix the financing and let people choose.*
|
||||
|
||||
**Don't argue it in dollars.** The dollars framing ($150k/head, hungry kids) is exactly what leaves the model vulnerable to *"then just give them cash; a big enough UBI is simpler."* If ownership is only an *instrument* for moving money, a UBI substitutes for it.
|
||||
|
||||
It isn't. It's the goal. **People deserve to govern the thing they spend their life inside.** The objection is *domination*, not just poverty. That claim is about human freedom, it survives the UBI, and no amount of cash dissolves it.
|
||||
|
||||
**And don't overclaim.** The pilot proves business viability or it doesn't. Don't promise that it will reshape the labor market. Say that's what success at scale would do, and let the data earn the rest.
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: The Bank
|
||||
section: Growing the Sector
|
||||
order: 50
|
||||
summary: A small public bank that lends only to worker co-ops, at market rates, with professional underwriting and published results.
|
||||
---
|
||||
|
||||
# The Bank
|
||||
|
||||
**A small public investment bank that lends only to worker cooperatives.**
|
||||
|
||||
Why a bank and not a mandate: co-ops are rare, and there are two competing explanations. One says they're rare because capital markets won't fund firms whose owners can't sell equity to outsiders. The other says they're rare because they're genuinely harder to run. A bank that removes the financing barrier is the experiment that tells those apart. If co-ops were starved of capital, they'll grow. If they're hard to run, the portfolio will fail, and we'll have learned that honestly and cheaply.
|
||||
|
||||
Mondragon's history supports the first half of the premise. For decades, apart from some government job-creation money, its own bank, Caja Laboral, was its co-ops' only source of capital from outside their own members. But that bank also gave them cheap loans and intensive hands-on help, in a market protected by tariffs, so nobody can say which one mattered (see [Has This Been Tried?](precedents.md)).
|
||||
|
||||
**Design:**
|
||||
|
||||
- **Charter:** lends only to firms that meet the [co-op definition](worker-ownership.md) and accept [the lock](the-lock.md). The bank can't widen its own charter. Mondragon's bank drifted into ordinary lending after a change in the law, and America's co-op bank drifted toward the safest co-ops once nothing held it to its target (see [Has This Been Tried?](precedents.md)). A charter can't stop Congress from changing it, but the fixed measures and deadline in [The Test](the-test.md) mean a mission change can't happen quietly. Drift inside the charter, toward the safest co-ops, is handled by money set aside for co-ops in lower-paid work and results published by group ([Who the Bank Lends To](who-gets-loans.md)).
|
||||
- **Priced at market rates.** This is deliberate. The bank's edge must be *willingness* to underwrite a firm structure other lenders refuse, not *cheap money*. If it lends below market, every result is unfalsifiable: success gets dismissed as "free state money," failure gets dismissed as "hobbled by the lock." At market rates, survival and returns mean something.
|
||||
- **Professional, insulated underwriting.** Loan officers are career bankers hired by the board, not political appointees. The board can overrule them only in writing and in public. Lending decisions are walled off from legislators the way monetary policy is walled off from Congress. A bank that funds whichever co-op has the best-connected sponsor fails the experiment and deserves to. Since 2026 the Supreme Court has made clear that any president can replace a board like this one at will, so the protection lives in what the board can't change, not in who sits on it (see [Making It Last](durability.md)).
|
||||
- **Self-sustaining by design.** One-time capitalization, then it lives on its loan book. Losses are public and published. A permanent appropriation would turn it into a subsidy line, and subsidy lines get captured.
|
||||
- **Full transparency.** Portfolio data (default rates, survival, wages, productivity, member retention) published annually, so anyone can check the result, including critics.
|
||||
|
||||
The actual numbers (how much money, how many co-ops, loan limits, and how "market rate" is checked) are in [The Pilot, in Numbers](pilot-spec.md). The rules against sham co-ops and political favoritism are in [Guarding the Pilot](pilot-safeguards.md). Loans for owners who choose to sell their company to its workers run on a separate track, described in [When an Owner Wants to Sell to the Workers](buyouts.md).
|
||||
|
||||
**Precedent:** this has been tried, in pieces. Congress created a National Consumer Cooperative Bank in 1978, but its law was written for consumer co-ops, and after it was privatized in 1981 it became mainly a lender to housing co-ops. Italy's Coopfond, funded by 3% of co-op profits, has lent and invested in co-ops for three decades and still holds about 91% of what it collected. Italy's Marcora Law puts public money alongside workers' own to fund worker buyouts. The details, including how each went wrong, are in [Has This Been Tried?](precedents.md).
|
||||
|
||||
**And a warning:** Uruguay's public fund for worker-run firms, FONDES, lost almost 90% of its loan book within five years, mostly on rescues of already-failed firms with political connections. [The pilot](pilot-spec.md#two-warnings-from-real-life) is designed around that failure.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Ownership and the Floor
|
||||
section: Beyond Co-ops
|
||||
order: 110
|
||||
summary: Ownership ends the skim, but a floor ends the hunger. Low-margin work needs both, and the floor can't wait for the co-op sector to grow.
|
||||
---
|
||||
|
||||
# Ownership and the Floor
|
||||
|
||||
This is the correction that matters most, and it took the longest to see.
|
||||
|
||||
**Equal ownership within firms does not produce equal outcomes across them.**
|
||||
|
||||
A worker-owned cleaning company still operates on cleaning-company margins, maybe $8k of profit per head. A worker-owned software firm throws off $150k per head. Both workers are owners. One of them still can't feed their kids.
|
||||
|
||||
Profit-per-head is set by technology and demand elasticity, forces ownership rules don't reach. Cleaning productivity has barely moved in forty years: one person, one mop. Ownership of the surplus doesn't change how much surplus *exists*.
|
||||
|
||||
So:
|
||||
|
||||
- **Ownership ends the skim.** It is the right answer to *extraction*: a large surplus flowing to people who didn't make it.
|
||||
- **A floor ends the hunger.** The home health aide, the childcare worker, the line cook: their problem was never that private equity was skimming their $150k. Their problem is there is no $150k.
|
||||
|
||||
You need both. They do different jobs. And because the model doesn't force anyone to convert, this section matters *more*, not less: the co-op sector will grow slowly, if it grows at all, and the floor doesn't wait for it. **One in six Utah kids goes hungry, and a lot of their parents work in sectors where there is barely any surplus to own.**
|
||||
|
||||
The floor, in pieces:
|
||||
|
||||
- A strong minimum wage.
|
||||
- **Targeted subsidy for essential low-margin work, paid to workers, not firms.** Somebody has to clean the nursing home for the poor. If the sector can't pay, the public does, directly, so the buyer can't absorb the subsidy by bidding less. Who decides what counts as essential, and how to keep every low-wage industry from claiming it, is in [Who Decides What's Essential](essential-work.md).
|
||||
- **Funded automation research aimed at exactly those sectors.** If productivity can't rise on its own, raise it. This is explicitly a *transition* strategy: support the workers until the technology lands. Say that out loud, or you end up permanently subsidizing a job you're simultaneously trying to eliminate, and the subsidized workers start lobbying against the automation. (This is what happened with coal.)
|
||||
- **A transitional public temp corps.** Companies apply for subsidized labor. The state supplies temps and funds the automation in parallel. Everyone knows going in that the job has a sunset. Voluntary, real wages, open to anyone who wants a bridge.
|
||||
|
||||
**The endpoint:** a society where work is not the price of being allowed to eat. Where people who don't work are taken care of, and people who work are people who want *more*: the finer things, a bigger share, the reward for doing what's hard or what nobody else will do.
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
title: The Lock
|
||||
section: The Co-op
|
||||
order: 30
|
||||
summary: Any co-op funded by the bank stays a co-op forever. It can't be sold to conventional owners or cashed out by its members.
|
||||
---
|
||||
|
||||
# The Lock
|
||||
|
||||
**Any firm funded by the bank stays a worker co-op forever. It can never be bought, sold, or converted into anything else.**
|
||||
|
||||
The lock is what keeps a successful experiment from being quietly bought back. Without it, the best-performing co-ops are the likeliest takeover targets, and every success becomes an acquisition. The lock exists so that wins accumulate instead of being harvested.
|
||||
|
||||
What it covers:
|
||||
|
||||
- **No sale to a non-co-op.** A co-op may merge with or be absorbed by another locked co-op. It may never be sold to conventional owners.
|
||||
- **No demutualization.** Members cannot vote to dissolve the co-op and split the value. (This is the failure mode that ended many US mutual savings banks and UK building societies: a generation of members cashed out what earlier generations built.)
|
||||
- **No outside voting equity.** Outside money comes in only as voteless, expiring claims (see [Capital, After](capital.md)).
|
||||
- **Membership requirement.** A locked co-op cannot run on hired non-member labor. Workers become members after a defined probationary period (at most 12 months), automatically unless they opt out. In jobs where most people leave within a year or two, that takes a shorter probation ([When People Don't Stay Long](high-turnover.md)). This blocks the "degeneration" pattern, where a co-op becomes a small ownership club employing a large wage-labor underclass. Mondragon's non-member foreign subsidiaries are the warning case.
|
||||
- **Indivisible reserves.** A slice of every year's profit goes to a collective reserve no member can ever withdraw. On dissolution, after debts are paid, remaining assets go to the federation fund, not to the members. Italian co-op law already works this way. It removes the incentive to kill the co-op for its assets.
|
||||
|
||||
What the lock does *not* do: keep failing firms alive. A locked co-op can still go bankrupt, and its assets can still be sold in liquidation. It just can't be sold as a going concern to anyone but another co-op.
|
||||
|
||||
## Why "forever" isn't as strange as it sounds
|
||||
|
||||
A permanent lock sounds extreme. But plenty of organizations already live under one, and the law already knows how to handle them.
|
||||
|
||||
- **Britain's community interest companies.** Since 2005, UK law has offered a kind of company whose assets are locked to its purpose. Official guidance is blunt: "once a company is a CIC it cannot become an ordinary company" ([UK government guidance](https://www.gov.uk/government/publications/community-interest-companies-how-to-form-a-cic/community-interest-companies-guidance-chapters)). A dedicated government regulator enforces the lock. That hasn't scared people off. There were more than 37,000 of them in 2025, and a record 8,376 were approved that year ([CIC Regulator annual report, 2024–25](https://assets.publishing.service.gov.uk/media/68809f4f28f29c99778a7504/cic-25-01-community-interest-companies-annual-report-2024-2025.pdf)).
|
||||
- **Companies that can't be sold.** Some well-known firms, including Bosch, Zeiss and the search engine Ecosia, are "steward-owned": set up so that control can't be sold to outsiders. Bosch and Zeiss do it through foundations. Ecosia uses a simpler device: an independent body holds a single "golden share" whose only job is to veto any change that breaks those rules ([Wikipedia](https://en.wikipedia.org/wiki/Steward-ownership)).
|
||||
- **American nonprofits.** Assets given to a charity stay charitable. When Blue Cross of California moved its business into a for-profit company in the 1990s, state regulators made it hand over more than $3.2 billion to two new health foundations. Its first offer had been $100 million ([Community Catalyst, 2004](https://www.communitycatalyst.org/wp-content/uploads/2022/11/conversion_and_preservation_of_charitable_assets_of_blue_cross_and_blue_shield_plans_mar04.pdf)). Notice what happened there, though: the company *did* change form. What the law protected was the value.
|
||||
|
||||
There's one honest difference. The founders of a community interest company, or of Ecosia, chose their lock freely. Here the lock is the condition of a public loan. Founders still choose it, since nobody has to borrow from the bank, but they choose it as part of a deal.
|
||||
|
||||
**And the failure the lock prevents is real.** Britain's building societies were member-owned mortgage lenders. From 1989 on, several of the biggest (Abbey National, Halifax, Northern Rock, Bradford & Bingley and others) turned themselves into banks. They won their members' votes by paying out windfalls from reserves that earlier generations had built, and speculators opened small accounts just to collect. Every converted society ended up swallowed by a bigger bank or failed ([Co-operative News, 2014](https://www.thenews.coop/big-bang-demutualisation-building-societies-failed/)). Northern Rock, a building society until 1997, suffered Britain's first bank run in well over a century in 2007 and was nationalized the next year ([Wikipedia](https://en.wikipedia.org/wiki/Northern_Rock)). Those failures had many causes, and mutuals can fail too. But the windfall votes show exactly the incentive the lock removes: current members cashing out what their predecessors built.
|
||||
|
||||
## How the lock is made to hold
|
||||
|
||||
A lock written only into a co-op's own rules can be voted away by its members. A lock written only into federal law can be repealed by Congress. So it's built in layers, and each one is meant to hold if another fails.
|
||||
|
||||
1. **A lock share.** Each locked co-op issues one special share to an independent **lock-holder**. The share pays nothing. It votes on only four things: a sale or merger, a change to the lock clauses, a payout on dissolution, and a sale of a large part of the business (below). On those, it has a veto. It **can't approve** a sale to a conventional buyer, because nobody can. And it **must approve** anything that follows the lock, such as a merger with another locked co-op, so it can't hold good deals hostage. A refusal can be challenged in court. State corporate law already allows restrictions of this kind. Delaware, for example, lets a company require consent before its shares change hands, or bar transfers to designated kinds of buyers, as long as the restriction isn't "manifestly unreasonable" ([Delaware Code, title 8, § 202](https://delcode.delaware.gov/title8/c001/sc06/index.html)). This is the same device Ecosia uses.
|
||||
2. **The co-op's founding documents.** The lock clauses are written into the articles, and they can be changed only with the lock share's consent. (Articles that simply claim they "can never be amended" may not hold up under state law. A required consent is what actually does the work.)
|
||||
3. **The law.** The bank's authorizing statute makes the lock a condition of every loan and says it outlasts repayment. It also changes federal bankruptcy law for locked co-ops (see below). Where states are willing, the lock goes into state co-op law too, through a short model law ([The Lock in State Law](lock-state-law.md); see also [The Constitution](constitution.md)).
|
||||
4. **A backstop that follows the money.** If a court or a later Congress ever breaks the lock and a co-op converts or is sold anyway, the reserves and any sale value above members' own account balances still go to the federation fund, not to the members. Italy works this way ([Has This Been Tried?](precedents.md#italys-co-op-funds-coopfond-and-the-marcora-law)). So even if the lock on the firm's form fails, nobody gets a windfall from breaking it.
|
||||
|
||||
**Who holds the lock share.** Each co-op picks its lock-holder from a list of approved ones: its [federation](federation.md), or an independent nonprofit lock trust with its own endowment and a successor named in case it ever shuts down. The rules keep the lock-holder honest:
|
||||
|
||||
- **It can't be a buyer, or benefit from a sale, in any deal it reviews.** If the co-op's federation might absorb the co-op, or would receive its assets on dissolution, a different lock-holder reviews that deal.
|
||||
- **It isn't the only one who can enforce the lock.** Any member, any other locked co-op, the bank and the state attorney general can also go to court.
|
||||
- **It publishes an annual review** of the co-ops it holds shares in.
|
||||
- **It checks each co-op at least every two years,** including private talks with a random group of workers, for a fee on a scale the bank approves ([What the Rules Cost a Small Co-op](compliance.md#the-lock-holders-review-every-two-years)).
|
||||
- **It also guards membership.** Beyond its four votes, it has one more job: it takes workers' decisions to opt out of membership and checks each co-op's membership records, so that the membership requirement means something ([When People Don't Stay Long](high-turnover.md)). It has no say over who is hired, fired or paid what.
|
||||
|
||||
**Where the legal risk really is.** Courts are wary of "restraints on alienation": conditions that stop an owner selling property, especially forever ([Cornell Legal Information Institute](https://www.law.cornell.edu/wex/restraint_on_alienation)). For company shares, reasonable restrictions are generally allowed, so that risk is manageable. **The bigger threat is bankruptcy.** Federal bankruptcy law generally overrides contract terms that restrict transferring a bankrupt company's property ([11 U.S.C. § 541(c)(1)](https://www.law.cornell.edu/uscode/text/11/541)). Without a change to that law, a court-run sale could hand a failed co-op's whole business to any bidder, lock or no lock. So the bank's statute has to amend the Bankruptcy Code for locked co-ops. That needs Congress. [When a Locked Co-op Goes Bankrupt](bankruptcy.md) lays out the amendment, modeled on how bankruptcy law already treats charities.
|
||||
|
||||
**What about members who never agreed to it?** Nobody is forced to work at a locked co-op. The lock is explained in writing when someone is hired and again when they become a member. The trade is the same as joining a nonprofit: you get a vote and a share of the profits in a firm you didn't found, and you can't sell it.
|
||||
|
||||
## When a co-op needs to change hands
|
||||
|
||||
The lock stops a co-op being sold to conventional owners. It doesn't freeze it in place. A co-op can still:
|
||||
|
||||
- **Merge with or be bought by another locked co-op**, including a much bigger one.
|
||||
- **Split off part of its business** as a new locked co-op, or sell that part to one.
|
||||
- **Sell things it no longer needs**, like old equipment, a building or a patent it doesn't use, to anyone at a fair price. The gain goes to the reserve (see [Investing for the Long Run](the-long-run.md#the-rules)).
|
||||
- **Take outside money without giving up control**, on voteless, expiring terms (see [Capital, After](capital.md)).
|
||||
- **Get help from its federation**, which can lend to a struggling co-op or help find a co-op buyer (see [The Federation](federation.md)).
|
||||
|
||||
## Closing the back doors
|
||||
|
||||
A lock on the company is worthless if the business can walk out the side door. These rules are part of the lock itself, so they last as long as it does, not only while a bank loan is outstanding.
|
||||
|
||||
- **Selling the business piece by piece counts as selling it.** If a co-op sells, leases or licenses more than a quarter of its assets or its revenue-producing business within any three years, counted together, that's treated as a sale of the business, and the lock share can veto it. The same goes for the brand, the core technology, or the main customer contracts, whatever their size.
|
||||
- **Buying the assets and hiring the workers counts too.** If a buyer of the co-op's assets hires more than a third of its members within two years, the deal is treated as a sale of the whole business after the fact. The buyer owes the federation fund the difference between what it paid and the value of the business as a going concern. The same goes for anyone who gets the use of what the co-op leased, licensed or financed and runs the same business with a third of its workers or its main customers. Leases and licenses of what a co-op can't run without carry the lock's terms too ([When a Locked Co-op Goes Bankrupt](bankruptcy.md#everything-else), point 13). People are always free to change jobs. This rule makes a buyer pay full price for a whole business, even if it picks the business up in pieces.
|
||||
- **Subsidiaries are covered.** Any company a locked co-op controls is also locked, and its workers are covered by the membership rule. Otherwise a co-op could grow through an ordinary subsidiary and then sell it. That's the pattern behind Mondragon's non-member foreign subsidiaries. How that works, including when a co-op buys a company, is [below](#when-a-co-op-owns-another-company).
|
||||
- **No outside control through the back door.** The limits in [Capital, After](capital.md) (outside claims last at most 15 years, rank behind lenders, and carry no vote) apply forever, not just while a bank loan is outstanding. No investor, lender, supplier or licensee can hold a veto over the co-op's budget, hiring or management. No contract can give one outside party an exclusive claim on most of what the co-op produces for longer than five years.
|
||||
- **Insiders can't drain it through contracts.** Rent, fees and prices paid to insiders are checked for as long as the lock lasts, not just while the bank's loan is outstanding ([below](#deals-with-insiders)).
|
||||
|
||||
## Deals with insiders
|
||||
|
||||
While a co-op owes the bank, every contract with an insider is checked each year, and overpaying one is a breach of the loan ([The hidden boss](pilot-safeguards.md#the-hidden-boss)). Once the loan is repaid, that lever is gone. A founder who kept the building could simply wait, then raise the rent. The lock is meant to stop the firm's value leaving through the back door, and an inflated contract is a back door. So the check becomes part of the lock.
|
||||
|
||||
Members take most of the profit, so they have every reason to refuse an inflated contract themselves, which a charity's donors don't. The danger is narrower: the insider controls what members see, or the co-op can't walk away. So the first defenses are telling members in advance and making sure they can leave a bad deal. Outside checking comes second.
|
||||
|
||||
The rules borrow from how federal law polices charity insiders ([26 U.S.C. § 4958](https://www.law.cornell.edu/uscode/text/26/4958)), because the tax law has decades of practice drawing this line. A charity's assets are dedicated to its purpose much as the reserve is dedicated to the people who'll work at the co-op later.
|
||||
|
||||
- **Who counts as an insider.** Anyone who in the last five years was a founder, board member, officer or manager, or was otherwise able to steer the co-op; their family; and any company they, together, own more than 35% of. The charity rule treats founding the organization as one sign of influence; this model counts founders and managers automatically, so it's broader. Anyone who signed a contract with the co-op while an insider stays one for that contract and its renewals, however long it runs, so stepping down and waiting five years doesn't work.
|
||||
- **Through a middleman counts.** If the co-op overpays a supplier who passes part of it on to an insider, it's an insider deal, as it is for charities ([26 C.F.R. § 53.4958-4(a)(2)(iii)](https://www.law.cornell.edu/cfr/text/26/53.4958-4)). So is a swap: if an insider of one co-op deals with a second co-op while an insider of the second deals with the first, both deals count.
|
||||
- **Members hear about it first.** Every insider deal worth more than $5,000 a year is sent to all members before it's signed, with the price, the terms and the evidence for them, and listed again in the yearly report to members and the lock-holder. Smaller ones are only listed.
|
||||
- **A fair process earns a presumption.** A deal is presumed fair if it's approved in advance by people with no stake in it, who had evidence of fair value and wrote down their reasons at the time ([26 C.F.R. § 53.4958-6](https://www.law.cornell.edu/cfr/text/26/53.4958-6)). Having no stake means not being the insider or their family, not working under them, not having one's pay set by them, having no money riding on the deal, and not having a deal of one's own that they approve. For pay, prices from three comparable employers nearby are enough in a co-op with sales under $1 million, as the charity rule allows for small organizations. For rent, buildings, equipment and licenses, the charity rule looks to an independent appraisal or real competing offers, and so does this one. Where a deal runs more than $5,000 a year, the appraiser comes from a list the lock-holder keeps, not one the insider picks, and the co-op pays. The accountant then checks the paperwork, not the price. A deal without that approval gets the full price check.
|
||||
- **When nobody is free to judge.** In a small co-op, everyone may work under the manager who owns the building. If there aren't at least three members with no stake, the deal needs an appraiser from the lock-holder's list, and the lock-holder checks the process.
|
||||
- **The co-op can always walk away.** Two of the pilot's rules become part of the lock too. Members can end any management or franchise contract by vote on short notice. And a co-op that leases something it needs to operate from an insider has an option to buy it at an appraised price ([The hidden boss](pilot-safeguards.md#the-hidden-boss)). A market price can still be steep, but an insider can't hold the co-op hostage at renewal.
|
||||
- **Overpaying costs the insider, not the members.** If a deal cost the co-op more than it would have paid a stranger, the insider repays the difference with interest, plus a quarter of it on top. If it isn't repaid within a year, they owe twice the excess on top instead. Charity law charges an insider those amounts as taxes (§ 4958(a)(1), (b)); here they go to the co-op. The insider and their company are both liable, so an empty shell company doesn't end it. The co-op can take what it's owed out of anything it owes the insider under any contract. The reserve's 30% comes out of what's recovered, not the members' profit pool. If nothing can be recovered after a real attempt, the loss is written off and falls on everyone, like any other bad debt. A member who's also an insider is covered by this rule, not by the pay rule, so the same money isn't charged twice.
|
||||
- **Approvers are liable only if they knew.** Board members who approved a deal knowing it was overpriced owe the co-op a tenth of the excess, up to $20,000 per deal, the share and limit charity law puts on charity managers (§ 4958(a)(2), (d)(2)), and their own payouts stop until it's paid. Following the process above, or relying on written, reasoned advice from a professional on the lock-holder's list, counts as not knowing, as it does for charity managers ([26 C.F.R. § 53.4958-1(d)(4)(iii)](https://www.law.cornell.edu/cfr/text/26/53.4958-1)). The federation's group insurance covers the cost of defending an approver who wasn't found to have known. Most worker-directors earn ordinary wages, and this is meant to deter insiders, not scare them off the board.
|
||||
- **The members can't wave it through.** Members can pay themselves whatever they decide, and [rule 1](the-long-run.md#the-rules) handles that. What they can't do is hand the co-op's money to someone outside it. An overpayment to an insider who isn't a member is an outside claim on the co-op, and outside claims are allowed only on the terms in [Capital, After](capital.md). An insider's investment in the co-op is an insider deal too: its share of profit has to be priced like a stranger's, and the lock-holder must approve it.
|
||||
- **Who checks once the loan is repaid.** The yearly insider test stays in the accountant's package. The lock-holder takes over the bank's part: it keeps the list of approved accountants and appraisers, gets the report, picks the samples and orders a check when [a warning sign](compliance.md#audits-by-lot-and-when-something-looks-wrong) appears, such as payments to insiders growing faster than sales, any insider's price rising faster than inflation, any renewal of an insider's lease, or when any member asks. Nobody tied to the insider can take part in the lock-holder's review of that co-op. A co-op with no insider deals over $5,000 a year isn't drawn. Among the rest, one in ten is drawn by lot each year for a check of its insider deals, not a full audit. The check is paid through the fees of co-ops with insider deals, and by an insider found to have overcharged. One in ten is a judgment. It's about what the charity rule's tax gives up: if a quarter on top is the penalty and the chance of being caught is at least one in ten a year, a long contract overcharged year after year is likely to be found before it pays.
|
||||
- **Who can go to court.** The same parties who can enforce the rest of the lock: the lock-holder, any member, any other locked co-op, the bank and the state attorney general. What's recovered goes to the co-op, and a member who wins has their legal costs paid out of it first.
|
||||
|
||||
This is weaker than the check while the loan is out, because the bank's threat to call its loan was the strongest lever there was. An insider who has left and owns nothing the co-op can reach may never pay. A presumption can be gamed with a flattering appraisal. The charity rule has the same weakness, and the IRS can still rebut the presumption with better evidence. Here the lock-holder can too.
|
||||
|
||||
## When a co-op owns another company
|
||||
|
||||
The membership rule is what stops a co-op from becoming a small club of owners with employees under them. Owning other companies is the easiest way around it. Mondragon's co-ops grew abroad by building and buying ordinary companies. By 2016 they controlled about 100 foreign subsidiaries and joint ventures that weren't worker-owned. Only about half of Mondragon's businesses were co-ops, and only about a third of its employees were members, counting non-co-op businesses at home too ([Kasmir, 2016](https://globaldialogue.isa-sociology.org/articles/the-mondragon-cooperatives-successes-and-challenges)). Its appliance co-op, Fagor, bought the French maker Brandt in 2005, and the workers of its foreign purchases weren't offered ownership. Fagor went bankrupt in 2013 owing €800 million to €1.1 billion, after Spain's housing crash and years of cheaper competition ([Fagor, Wikipedia](https://en.wikipedia.org/wiki/Fagor)). The collapse had several causes. A big purchase adds risk, and more so when the buyer is carrying debt.
|
||||
|
||||
So "controlled companies are locked" needs rules for what control is, what happens to the people who come with a purchase, and how a co-op gets out of a bad one.
|
||||
|
||||
### What counts as control
|
||||
|
||||
- **More than half, or running it.** Holding more than half the votes or the value counts as control. So does the right to name most of the board, or running the company through a management contract. Stakes held by the co-op's managers, members' relatives, or companies they control are added to the co-op's own. Several locked co-ops holding a company together control it together, and the one with the largest stake is responsible for the rules below.
|
||||
- **Even partnerships.** In a 50–50 joint venture with an ordinary company, the co-op has control only if it runs the venture day to day. Otherwise the stake is an investment. That leaves room for joint ventures, and the hours rule below keeps them from becoming a workaround.
|
||||
- **Less than control is an investment.** A co-op can hold a smaller stake in an ordinary company as an investment of its reserve, like any investor.
|
||||
- **Dependence without ownership.** A co-op doesn't have to own a company to run it. It can franchise its business to others, or use a contract manufacturer that works mostly for it. It can't make an independent company convert, and the model doesn't try. Instead, the hours of any franchisee of the co-op, and of any supplier that sells most of its output to the co-op for two years running, directly or through a middleman, count in the co-op's hours test below. Ordinary buying from ordinary suppliers isn't covered, and isn't meant to be.
|
||||
|
||||
### What the co-op has to do
|
||||
|
||||
- **No lasting subsidiaries with workers.** Within three years of taking control, a controlled company with employees is either merged into the co-op or turned into its own locked co-op run by its own workers, like a [spin-off](growth.md). Its workers choose which, by secret ballot, once they've had the lock explained in writing. If it becomes its own co-op, the parent's stake becomes an investment on the usual terms: repayment plus a share of profit for at most 15 years, with no vote ([Capital, After](capital.md)). Any other shareholders are bought out first at an independent appraisal. A locked co-op run by its own workers meets this rule, though one that sells most of its output to the parent is counted with the parent for the hours test, as spin-offs are. A company with no employees, like one that only holds a building, can stay under the co-op's control. It's locked like any other asset.
|
||||
- **One hours test across the group.** The co-op itself meets the three-quarters rule as usual. Across the co-op, the companies it controls, and the franchisees and dependent suppliers above, members of locked co-ops have to do at least half of all hours, once the co-op's own phase-in is over. So a co-op can't buy companies one after another, or farm its work out, to keep a large workforce outside membership.
|
||||
|
||||
### The people who come with a purchase
|
||||
|
||||
- **Membership is offered, not imposed.** Acquired workers become members automatically after probation unless they opt out. Their time at the old company counts toward probation. Nobody acquired can be let go at the end of probation except for cause.
|
||||
- **Saying no is safe.** Someone who was working there on the day of the purchase and opts out keeps their job and pay, and their hours don't count against any hours test for as long as they stay. The co-op gains nothing by pushing them out.
|
||||
- **Unions and contracts carry over.** A buyer that keeps most of a unionized workforce generally has to recognize and bargain with its union ([NLRB v. Burns, 1972](https://www.law.cornell.edu/supremecourt/text/406/272)). The co-op recognizes the union and honors its contract. After that, recognition sticks unless most of the workers the union covers vote to end it, the rule the model already uses for recognized unions ([Labor Law](labor-law.md#union-rights-the-biggest-gap)). The company's pension promises stay with it. Where labor law requires something different, labor law wins.
|
||||
|
||||
### Buying, and getting out
|
||||
|
||||
- **Big purchases need the members.** Buying a company with more workers than the co-op has members, or paying more than a quarter of the reserve, needs a majority of all members, not just those voting, by secret ballot. The members can vote in advance on a ceiling for the price, so the terms don't have to be made public during the deal. The price includes any debt the co-op takes on, and purchases within three years are added together. The federation gives a written opinion first. It's advice, not a veto.
|
||||
- **Selling one back.** Any sale of a controlled company with employees goes through the lock-holder, whatever its size. The company's workers get the first chance to buy it as their own locked co-op. Within three years of the purchase, before it's merged or converted, the co-op can sell it to anyone else at an independent appraisal. Anything above what the co-op paid goes to the federation fund, so buying companies to run without members and then resell them doesn't pay.
|
||||
- **Abroad, the same rules, with a substitute.** Where local law has no form for a worker co-op with a lock, the lock-holder can approve a local structure that gives the workers the same control and the same limits on sale, like voting shares held in trust for them. If there's no such structure, the co-op can't keep control past three years. It can hold a smaller stake, or sell as above. That makes plants abroad much harder to run, and it costs co-ops some foreign business.
|
||||
|
||||
## When a co-op goes bankrupt
|
||||
|
||||
Argentina changed its bankruptcy law in 2011 so that when a firm fails, its workers can form a co-op, keep the business running, and buy it, counting what they're owed toward the price ([Law 26.684, articles 189, 203 bis and 205](https://www.argentina.gob.ar/normativa/nacional/ley-26684-183856/texto)). This model borrows the idea, adapted to American law. The full rules, and the amendment to federal bankruptcy law they need, are in [When a Locked Co-op Goes Bankrupt](bankruptcy.md). In short:
|
||||
|
||||
- **Only co-ops can buy the business whole.** Its own workers (as a new locked co-op), the federation, or any other locked co-op whose members do most of its work can bid. It's sold by open bidding among them. The minimum price is what the assets would bring sold off separately, set by an appraiser the court appoints.
|
||||
- **Workers can keep it running while it's sold,** and count the unpaid wages they choose to hand over toward their bid, up to the amounts bankruptcy law already puts first. The legal minimum wage and overtime aren't handed over. The savings insurance pool pays them in cash first, up to eight weeks.
|
||||
- **No buying back your own failure cheaply.** People who managed the co-op in the two years before it failed can't get bank financing to buy it back.
|
||||
- **If no co-op buys it**, the assets are sold off separately to anyone. But no single buyer and its related companies may assemble enough of the pieces to add up to the business.
|
||||
- **A reorganization can't hand over control.** If a locked co-op reorganizes in bankruptcy instead of closing, the plan can't give voting shares to anyone but members. Creditors who vote against the plan still get what the business would be worth on the open market. That value is tested by inviting outside offers, not just appraised.
|
||||
- **The buyer answers for the old co-op's products.** A co-op that buys the business takes on injuries from what the old co-op made, its warranty repairs and customers' deposits. So does a co-op whose members keep it through a reorganization.
|
||||
|
||||
**This does cost creditors something.** When only co-ops can bid on the whole business, the price may be lower than an open auction would fetch, and lenders will charge for that risk. Creditors who never chose to lend, like people the co-op injured, are paid the difference from the federation fund. That's part of the lock's price (below).
|
||||
|
||||
## What the lock costs
|
||||
|
||||
The lock has real costs, and they should be stated plainly.
|
||||
|
||||
- **Fewer buyers means lower prices, and sometimes a breakup.** When a failing co-op finds no co-op buyer, a business that a conventional buyer would have kept running may be sold off in pieces, and its workers lose their jobs. Lenders get back less, and they'll charge for it. The pilot publishes the bank's recovery rate on failed loans (see [The Pilot, in Numbers](pilot-spec.md#what-gets-measured)), so this cost shows up in the numbers.
|
||||
- **While the sector is small, so is the pool of co-op buyers.**
|
||||
- **Owning other companies is harder.** Joint ventures, purchases and plants abroad come with rules ordinary firms don't face ([above](#when-a-co-op-owns-another-company)). Deals are slower, some sellers and partners will walk away, and co-ops may lose business abroad to firms that can own plants there.
|
||||
- **Members can't take the firm's value with them.** That's the point of the lock, and it's also the heart of [the founder problem](founder-problem.md).
|
||||
- **"Forever" is a long time.** Critics on the right argue that a permanent lock, bought with one loan, binds generations who never chose it. Italy takes a softer approach: a co-op can become an ordinary company, but its accumulated reserves go to the co-op movement's fund, not to the members. Whether this model should follow Italy is an open question (see [Open Problems](open-problems.md)).
|
||||
@@ -0,0 +1,153 @@
|
||||
---
|
||||
title: Investing for the Long Run
|
||||
section: The Co-op
|
||||
order: 27
|
||||
summary: Why economists expected worker co-ops to underinvest, what the evidence actually shows, and the rules that keep members who are about to leave from starving the firm, running it down or wasting its reserve.
|
||||
---
|
||||
|
||||
# Investing for the Long Run
|
||||
|
||||
Picture a machinist who is 60 and plans to retire in three years. Her co-op is voting on a $2 million production line that will take four years to start paying off, then earn money for fifteen. Whichever way it's paid for, part of the cost comes out of the profits she shares in while she's still there, and almost all the benefit goes to people who work there after she's gone. Why would she vote yes?
|
||||
|
||||
That's the **horizon problem**: a member's stake in the firm lasts only as long as their job, but many good investments last much longer. In a conventional company the problem mostly doesn't arise. A shareholder who plans to sell next year still wants the company to invest well, because the share price reflects the future profits the investment will earn. A co-op member has no share to sell. [The lock](the-lock.md) makes sure of that.
|
||||
|
||||
This page explains why economists worried about this, what actually happens in real co-ops, and how this model handles it.
|
||||
|
||||
## The worry
|
||||
|
||||
In 1970 the economists Eirik Furubotn and Svetozar Pejovich, studying worker-run firms in Yugoslavia, argued that such firms would pay out too much and invest too little, because members can't take the value of an investment with them when they leave. The same year Jaroslav Vanek, one of the leading academic supporters of worker management, predicted underinvestment in firms that finance themselves from collective savings (both summarized in [Novkovic, *Journal of Socio-Economics*, 2008](https://www.theholmteam.ca/Novkovic-coopDifference2008.pdf)). Taken to its extreme, the prediction is that such firms slowly starve themselves of capital.
|
||||
|
||||
It's a serious argument, and the design has to answer it rather than hope it away.
|
||||
|
||||
## What actually happens
|
||||
|
||||
**The predicted underinvestment mostly doesn't show up in the data.**
|
||||
|
||||
- **France.** The largest comparison so far used two big, representative samples of French firms across many industries, including services. Worker co-ops were as productive as conventional firms or more so, used their equipment and labor efficiently, and weren't inefficiently small. Contrary to the textbook prediction, they grew as fast as conventional firms or faster ([Fakhfakh, Pérotin and Gago, *ILR Review*, 2012](https://ideas.repec.org/a/sae/ilrrev/v65y2012i4p847-879.html)).
|
||||
- **Italy.** A study of a large panel of Italian worker and social co-ops tested the prediction directly. It found the opposite of what the theory expects: the more of a co-op's capital was held in collective reserves that no member can withdraw, the *more* productive it was ([George, Fontanari and Tortia, 2019](https://ideas.repec.org/p/trn/utwprg/2019-20.html)).
|
||||
|
||||
**But there's a catch, and it matters.** These co-ops don't get to choose freely. Italian law requires every co-op to put at least 30% of each year's profit into a reserve that members can never take out ([Italian Civil Code, art. 2545-quater](https://www.brocardi.it/codice-civile/libro-quinto/titolo-vi/capo-i/sezione-iv/art2545quater.html); [Lomuscio and Tortia, 2023](https://pdfs.semanticscholar.org/a7eb/90b586cb2adb63c321f1ca616afefb27eb70.pdf)). Mondragon did the same thing by its own rules. So the honest reading isn't "the horizon problem is a myth." It's "**a required reserve seems to be enough to keep it in check.**"
|
||||
|
||||
Mondragon's history shows the pressure is real. When its co-ops set their own reserve levels, the share of profit going into reserves swung from 35–53% in the 1960s down to 11–29% in the 1970s. One co-op, Alecoop, which employs students who leave every three years, faced losing its capital as each group took its money out, so it raised its reserve share to 60%. In 1989 Mondragon raised the minimum reserve for all its co-ops from 20% to 50% of profit ([Morris, *The Mondragón System*, 1992](https://www.ilsr.org/wp-content/uploads/2013/03/mondragon-3.pdf)). Where the horizon pressure bit, the answer was a bigger required reserve.
|
||||
|
||||
## The fix that backfired: selling your place
|
||||
|
||||
There is an obvious way to solve the horizon problem completely: let members sell their membership when they leave. Then, like a shareholder, a retiring member benefits from every good investment, because it raises the price of their place.
|
||||
|
||||
The plywood co-ops of the Pacific Northwest did exactly this. Each member owned one share, and shares could be sold to new members. For decades they were among the most durable worker-owned firms in American manufacturing. Economists who later studied the share prices found membership had been extremely profitable ([Craig and Pencavel, *American Economic Review*, 1992](https://ideas.repec.org/a/aea/aecrev/v82y1992i5p1083-105.html)).
|
||||
|
||||
Then success worked against them. Workers nearing retirement found their shares had risen from $1,000–2,000 to $25,000, $50,000 or more. Young workers couldn't afford to buy in, so retiring members sold to conventional lumber companies instead ([Lindenfeld and Wynn, 1995](https://geo.coop/articles/why-some-worker-co-ops-succeed-while-others-fail)). Observers called it "the failure of success." It wasn't the only cause of their decline. The whole plywood industry moved south from the 1960s on, and many conventional mills in the Northwest closed too ([Pencavel, lecture notes on worker co-ops, 2018](https://cleo.rutgers.edu/wp-content/uploads/2020/03/John-Pencavel-Notes-for-Students_WorkerCoops2018A.pdf)). But the co-ops that were sold while still profitable were sold because their members could sell them.
|
||||
|
||||
The retiring plywood workers weren't doing anything wrong. They were making a free choice with property they legally owned. This model simply doesn't create that property. A tradable membership turns the co-op back into something owned and sold, which is exactly the claim [the thesis](thesis.md) says this kind of firm exists to prevent. And it prices the next generation of workers out, which is how the plywood mills ended up being sold.
|
||||
|
||||
## Who pays for an investment, and when
|
||||
|
||||
The horizon problem bites when the members voting today pay for an investment whose benefits arrive after they leave. How much it bites depends on where the money comes from.
|
||||
|
||||
**1. The reserve.** At least 30% of every year's profit goes into the co-op's permanent reserve (see [Member Capital Accounts](capital-accounts.md#where-each-years-profit-goes)). No current member can ever take that money home, so paying for the line this way doesn't cut our machinist's share this year. (The line's cost still counts against profit a little each year, as point 3 explains.) The reserve is money that already belongs to the co-op's future, and it gets spent on the co-op's future.
|
||||
|
||||
**2. Members' accounts.** Money members leave in their [capital accounts](capital-accounts.md) isn't lost when they leave. It's paid back to them with interest. So if the co-op keeps more of this year's profit in members' accounts to pay for the new line, the machinist gets her money back, by default five years later, whether or not she's still there. Her real cost is the wait, and the risk that the co-op fails. Five years is shorter than most machines last, so for long-lived investments this helps less than it would if accounts were held until members left. Federal pension law is why they aren't ([Member Capital Accounts](capital-accounts.md#why-not-pay-it-out-when-you-leave)).
|
||||
|
||||
**3. Spreading the cost over time.** A business doesn't count the whole price of a machine as a cost in the year it's bought. It spreads the cost over the machine's working life. A $2 million line that lasts 20 years counts as $100,000 of cost a year. This is called *depreciation*, and it's how ordinary accounting already works. The profit split is based on profit after depreciation, so the machinist isn't charged for the years after she leaves. The bank does the same with repayment: loans for buildings and long-lived equipment are paid back over the asset's working life, up to 25 years for real estate, the same limit the SBA's main loan program uses ([SBA, types of 7(a) loans](https://www.sba.gov/partners/lenders/7a-loan-program/types-7a-loans)).
|
||||
|
||||
**This doesn't fix everything.** Depreciation spreads the cost evenly, but the payoff isn't even. If the line takes four years to start earning, the machinist is charged for three years of it and sees none of the benefit. That's the residual horizon problem, and it's why the reserve matters so much. The investments with the slowest payoff are the ones the reserve should pay for.
|
||||
|
||||
## Where the gaps are
|
||||
|
||||
That leaves four ways the horizon problem can still get in.
|
||||
|
||||
- **Paying it out as salaries.** The reserve rule applies to *profit*, and members set their own salaries. A co-op could raise everyone's pay until there's no profit left, and the reserve would get nothing. Newer members have an extra reason to like this: their profit share starts small and grows with tenure, but their salary is paid in full from day one.
|
||||
- **Pay that isn't salary.** Pensions promised for the future, retiree health cover, generous severance. These push costs onto whoever works there later.
|
||||
- **Selling off the furniture.** The lock stops anyone selling the *firm*, but not its assets. Members about to leave could sell the building, book the gain as profit, split it, and leave their successors paying rent.
|
||||
- **Development that pays off late.** Designing a new product or building software costs money now and pays later, but it's usually counted as a cost in the year it's spent. So the members voting today pay for all of it.
|
||||
|
||||
## The rules
|
||||
|
||||
**1. The reserve's share applies to pay above the going rate, too.** This isn't a cap on wages. Members can pay themselves whatever they decide. But the reserve takes its 30% of everything members receive above the going rate, whether it's called profit or pay.
|
||||
|
||||
- **The going rate for each job** is the higher of two numbers: the median pay for that occupation in the co-op's own metro area, from the government's wage survey (the Bureau of Labor Statistics' [Occupational Employment and Wage Statistics](https://www.bls.gov/oes/)), or the local living wage for a single adult working full time, from [MIT's Living Wage Calculator](https://livingwage.mit.edu/). Both are measured per hour, so part-time work and consultants are compared fairly. The living-wage floor matters. In home care the median wage can be close to poverty pay, and a co-op that raises its aides to a living wage shouldn't owe anything for doing it.
|
||||
- **Why a single adult's living wage.** In Salt Lake City in 2026, MIT puts the living wage at $24.64 an hour for a single adult and $41.55 for one adult supporting a child. An earlier version of this rule let pay up to one and a half times the second figure, about $130,000 a year, count as "raising the bottom." That would have let a co-op pay every cleaner six figures without the reserve getting anything. The single-adult figure, about $51,000 a year there, is still well above what most aides earn. Paying more than that is allowed. The reserve just takes its 30% of the difference.
|
||||
- **It's checked job by job.** Paying one group below the going rate doesn't make room to pay another group above it. An outside reviewer, the co-op's auditor or the bank's, assigns each person the occupation code for the work they spend most of their hours on, not their title, and the codes are shown to members next to each job. Where the survey has no figure for the metro area, the state figure is used, then the national one.
|
||||
- **People who work from home somewhere else.** More than one worker in five teleworked in March 2026 ([BLS](https://www.bls.gov/opub/ted/2026/22-6-percent-of-workers-teleworked-in-march-2026.htm)). The wage survey generally counts remote workers at the employer's reporting location, not at their home ([BLS survey instructions](https://www.bls.gov/respondents/oes/faqs.htm)). So the going rate for them still starts from the co-op's own area. What changes:
|
||||
- **The floor can only go up.** Everyone is paid at least the co-op's figure. For someone who lives elsewhere, the living-wage part of the floor is the higher of the co-op's living wage and the one where they live. The minimum wage and wage laws of the state they work in apply too, including higher salary thresholds for overtime. A move that raises the floor counts from the day of the move.
|
||||
- **The line for high pay can rise only for jobs hired as remote.** Suppose a co-op filled a job with a posting that advertised it as remote, and the person lives in a metro area where the survey's median for that job is higher than the co-op's. Then that person's pay counts as "above the going rate" only above their own metro's figure. So a co-op in a cheap area can pay what it takes to hire a remote engineer in an expensive city without the reserve taking 30% of the difference. A remote worker in a cheaper place gets no raised line.
|
||||
- **Never for insiders.** Board members, officers and managers never get the raised line, and neither does anyone who switches to remote after being hired. "Go remote" can't be used to move insiders' pay out of the reserve's reach.
|
||||
- **Fully remote means fully remote.** No more than 10 days a year at a co-op site, logged. Anyone who comes in more than that is held to the co-op's figures. The raised line is counted month by month, so it ends in the month someone stops qualifying.
|
||||
- **It's checked, not claimed.** The auditor checks the home address against a lease or state and local tax records, and checks the site logs. The co-op also reports its share of pay above the going rate without the raised line, so any use of it shows.
|
||||
- **The floor is what counts for tax.** The raised line only decides what the reserve takes. It isn't a claim about what pay is reasonable. The going rate that [the tax argument](taxes.md#wages-first) relies on is the floor.
|
||||
- **Contractors and workers abroad.** Pay to contractors doing a regular job for the co-op, and to staffing or employer-of-record firms supplying workers, counts as pay under this rule, both for the floor and for the line. Workers abroad are held to the co-op's own figure as their floor, with no raised line, and the auditor checks their contracts and payment records. That largely rules out hiring abroad to pay less for the same job. It's a choice: the going rate exists to stop pay being cut because of who does the work, and where they live is part of that. It costs co-ops some hires, and a co-op in an expensive city will hire fewer remote workers in cheaper places for the same reason.
|
||||
- **It counts total pay.** Employer retirement contributions above 6% of pay, bonuses, severance beyond two weeks' pay per year worked (up to 26 weeks), and perks like a car, housing or loans to members, valued at cost, all count as pay. So does pay to non-members and to people working through a related company, and the value of any deal with a member or their family on better than market terms, like buying from a relative's firm above the market price. Co-ops can't promise pensions or retiree benefits they don't pay for in full each year. The severance line is there to stop severance from becoming a payout to departing insiders, so it only helps people let go against their will: laid off, or dismissed for anything short of serious misconduct. If the job is filled again within a year, it wasn't a layoff. Severance on retiring or quitting counts as pay in full. Anything the law requires never counts as pay. The weeks are valued at the person's average pay over their last three years, as New Jersey does, so a raise just before leaving doesn't inflate them. For comparison, New Jersey requires one week per full year in mass layoffs and closings ([P.L.2019, c.423](https://pub.njleg.state.nj.us/Bills/2018/PL19/423_.HTM)), and Ontario requires one week per year, up to 26 weeks, of larger employers once someone has worked there five years ([Ontario](https://www.ontario.ca/document/your-guide-employment-standards-act-0/severance-pay)). This line allows twice the rate per year, with the same 26-week cap. The factor of two is a judgment.
|
||||
- **How it's settled.** Thirty percent of the excess goes to the reserve, first out of the member's profit share for that year, before it's paid or credited, and then out of their [capital account](capital-accounts.md). No new cash has to be found, because the members already took that money as pay. Taking it from this year's share first avoids a second round of tax on money already credited ([How a Locked Co-op Is Taxed](taxes.md#capital-accounts)). If that isn't enough, the rest comes out of their future account credits, or out of their payout when they leave. For pay to non-members, it comes out of that year's members' profit pool.
|
||||
- **There's a precedent.** US tax law already treats salaries that aren't "reasonable" as disguised profit ([26 U.S.C. § 162(a)(1)](https://www.law.cornell.edu/uscode/text/26/162)), so owners can't dodge tax by calling profit pay. This rule stops the same disguise being used to dodge the reserve. The tax test is judged case by case. This one is mostly a fixed formula, so it's cruder but much harder to argue about. The one judgment call is who counts as fully remote, and the auditor checks that against records.
|
||||
|
||||
**2. Gains from selling assets go to the reserve.** If a co-op sells a building, equipment, a brand or a patent for more than its value on the books, the whole gain goes to the reserve, not into the profit split. A sale to an insider, or a sale-and-lease-back, is checked like any [insider deal](pilot-safeguards.md#the-hidden-boss).
|
||||
|
||||
**3. Development projects can be spread out, like machines.** A co-op can designate a specific development project *before* it starts spending on it: designing a new product or a better production process, or building software. For the profit split only, its cost is then spread over up to five years, the same way a machine's is.
|
||||
|
||||
- **Only real development qualifies.** It has to meet the test international accounting standards already use to decide when development creates a real asset: it's technically doable, the co-op intends to finish it and can, and it's likely to earn money ([IAS 38](https://www.ifrs.org/issued-standards/list-of-standards/ias-38-intangible-assets/)). Those standards specifically exclude training, advertising and start-up costs, and so does this rule. Those stay costs of the year they're spent.
|
||||
- **The people who chose it carry the risk.** If a project is abandoned or written down within five years, the cost that hasn't yet been charged comes out of the capital accounts of the people who were members when it was designated, including former members whose payouts haven't been made yet. Each credit waits three to seven years before it's paid, so much of the money is usually still there. That makes the rule useless for pushing costs onto the next group of workers.
|
||||
- **Limits.** The total cost still waiting to be charged can't exceed half of the co-op's average profit over the last three years, measured before any spreading. Only co-ops with an independent audit can use the rule, and the auditor checks each open project every year. The lighter review small co-ops get otherwise doesn't count ([What the Rules Cost a Small Co-op](compliance.md#what-a-review-can-and-cant-stand-in-for)).
|
||||
- **It only changes how costs are divided between this year's members and future ones.** It doesn't touch the co-op's tax return or its reports to the bank. The co-op publishes both numbers side by side.
|
||||
|
||||
**4. Accounts earn a fair rate while they're at risk.** A current member's account earns the one-year Treasury rate plus two percentage points, credited every year, good or bad, so it's a fixed return on capital and not a share of profit ([How a Locked Co-op Is Taxed](taxes.md#interest-on-accounts)). Money in a current member's account is at risk: it shares in the co-op's losses and ranks behind its lenders. Paying only a risk-free rate on it would push every member to take cash instead. **Once a member leaves, the balance earns the plain Treasury rate.** It no longer shares in losses, so it's no longer at risk, and nobody should collect a premium from the work of people they no longer work alongside. For comparison, Italian law lets co-ops pay members up to 2.5 points above the rate on its postal savings bonds on the capital they've put in (Lomuscio and Tortia, 2023), and Mondragon paid 6% plus an adjustment for inflation (Morris, 1992). The details are on the [capital accounts page](capital-accounts.md#how-the-account-grows).
|
||||
|
||||
**5. The reserve doesn't pay for high pay in a loss year.** Mondragon calls its members' pay *anticipos*, advances on profits ([Morris, 1992](https://www.ilsr.org/wp-content/uploads/2013/03/mondragon-3.pdf)). This model treats the part of pay above the going rate the same way: as an advance against profit that hasn't been earned yet.
|
||||
|
||||
- **If the year ends in a loss,** the part of the loss up to the pay above the going rate, in that year and the year before, is charged to the people who got that pay, in proportion to how much each got. It comes out of their [capital accounts](capital-accounts.md), and out of their payouts if they've left since. The 30% rule 1 already moved from their accounts counts toward it, so nobody pays twice. The rest of the loss is shared as usual.
|
||||
- **An example.** A co-op loses $50,000. Its managers were paid $30,000 above the going rate that year, and rule 1 already moved $9,000 of that into the reserve. The managers' accounts are charged $21,000 more. The other $20,000 of the loss is shared between the reserve and all members' accounts in the usual way.
|
||||
- **What their accounts can't cover** comes out of their future profit shares, above the cash they need for tax, and nobody's above-going-rate pay can rise until it's repaid. After three years, anything still owed is written off. Three years is a judgment. Much shorter, and the best-paid could wait out a charge. Much longer, and one bad year would hang over people's savings for most of their time at the co-op. Whatever is written off falls on the reserve, which means on the people who work there later.
|
||||
- **Pay above the going rate to non-members** is charged to all members in proportion to their profit shares, since members chose to pay it. Members whose household income is under twice the poverty line are left out, and so is the insured slice of anyone's account.
|
||||
- **It never touches pay already earned.** Wages and bonuses already paid or earned stay paid. The charge falls on savings in the co-op, not paychecks.
|
||||
|
||||
Without this rule, members could vote themselves high pay, run the co-op at a loss, and let the losses eat the reserve once their own accounts ran dry. That would be withdrawing the reserve by another name, which [the lock](the-lock.md) forbids. The cost is real too. In an honest bad year, the best-paid people lose some savings, and some may leave.
|
||||
|
||||
## When members stop investing, or invest badly
|
||||
|
||||
The horizon problem has two mirror images. Members who are leaving soon might let the co-op run down. And because the reserve can never be paid out, members might spend it on expansion that doesn't pay.
|
||||
|
||||
### Running it down
|
||||
|
||||
Picture the worst case: a co-op whose members are mostly near retirement. What could they gain by letting it run down?
|
||||
|
||||
- **Pay can't drain the reserve** (rules 1 and 5), **and neither can selling off assets** (rule 2).
|
||||
- **Closing doesn't pay.** If the co-op dissolves, members get their own account balances, which they'd get anyway, and everything else goes to the [federation fund](federation.md) ([The Lock](the-lock.md)). Nobody gets a windfall from shutting a working firm.
|
||||
- **The co-op can stop hiring and shrink as people leave.** But workers it does hire join after probation, and members must do at least three-quarters of the hours ([When People Don't Stay Long](high-turnover.md)). It can't replace its members with cheaper employees.
|
||||
|
||||
That still leaves two real ways to run it down, and both come from how accounting works.
|
||||
|
||||
- **Skipped upkeep.** Repairs count as a cost in the year they're done, so skipping them raises this year's profit, and members share it.
|
||||
- **Machines that are "paid for."** Accounting spreads a machine's cost over its expected life, its *depreciation*. Once that time is up, the machine stops counting as a cost, even if it's still running. If nobody replaces it, profit looks higher every year, and members split the difference. That's not savings. It's the machine wearing out.
|
||||
|
||||
**6. Worn-out equipment keeps being charged.** For the profit split only, buildings and equipment that are fully written off but still in use keep being charged at their last yearly rate, adjusted for inflation. That money goes to the reserve, not the split. So a co-op running on old machines builds up the money to replace them, instead of paying it out. Co-ops whose buildings and equipment still in use, at what they originally cost and including leased ones, come to less than a fifth of their yearly revenue, averaged over three years, can skip this. Revenue here leaves out materials and subcontracted work passed on to customers at cost. Measuring at original cost means letting equipment run down can't bring a co-op under the line, and counting leased equipment means selling it and leasing it back can't either. For a rough sense of who that leaves out: on 2022 corporate balance sheets, buildings and equipment at original cost came to about 14% of sales in professional services, 8% for building contractors and 22% for specialty trades like plumbing, 22% in the group that includes home health care, 27% in cleaning and other building and office services, 37% at restaurants and 41% in manufacturing ([IRS, Table 1](https://www.irs.gov/statistics/soi-tax-stats-corporation-complete-report)). Those totals are weighted toward big companies, and the smallest ones don't report balance sheets, so they're a guide, not a measurement. A home care co-op that owns a few laptops is far under the line. The charge only falls on equipment that's fully written off anyway, so for a co-op near the line little turns on which side it's on.
|
||||
|
||||
There's no fair way to set how much every business should spend on upkeep, so this model doesn't try. Instead there's a warning light:
|
||||
|
||||
- **Every year, the co-op shows members how much it spent on new buildings and equipment**, including new leases and any unpaid invoices it has sold, next to how much its existing ones wore out that year, adjusted for inflation.
|
||||
- **If spending stays below wear for three years running, an independent inspector reports** on the condition of the main buildings and equipment. The lock-holder picks the inspector and the co-op pays. The report goes to every member, and to the bank if it's still owed money. After a buyout, buying the business puts its used equipment on the co-op's books at today's value, spread over the few years it has left, which makes it look like it's wearing out fast. So for the first five years, wear is measured on the seller's old book values instead. The upkeep the seller put off, which the [pre-sale check](buyouts.md#a-fair-price) already counts, is the starting point: spending that doesn't catch up on it counts as falling behind. A conversion without a sale doesn't re-value anything, so nothing changes for it.
|
||||
- **A co-op that's shrinking on purpose,** because its market is, says so in a written plan its members vote on. That's a warning light, not a spending order.
|
||||
|
||||
### Spending it badly
|
||||
|
||||
Critics worry that money members can never take home will feel free, and get wasted on expansion. Fagor, Mondragon's appliance maker, expanded across Europe and beyond, with plants in Poland, France, Morocco and China, much of it on borrowed money. Then Spain's housing crash took away its main market, and it went bankrupt in 2013.
|
||||
|
||||
The danger is real, but reserve money isn't free to members.
|
||||
|
||||
- **It earns money when it's not spent.** Reserve money that isn't used earns interest, and members share in that interest. Spending it on a project that earns less makes their share smaller. They only get about 60% of that interest, so it's cheaper to them than it should be. It isn't free.
|
||||
- **Failures come partly out of members' own accounts.** When a project fails, its value on the books is cut, and that counts as a loss. Losses are shared between the reserve and members' accounts in the same proportions as profit ([Member Capital Accounts](capital-accounts.md#how-the-account-grows)). The federation's share doesn't take losses, so members pay about 60 of every 90 dollars, two thirds. That holds only while they're still there, and only up to what their accounts hold, which is capped at a year's pay.
|
||||
|
||||
**Fagor wasn't mainly a reserve problem.** It owed about €800 million (about $1.1 billion) when it filed for bankruptcy ([The Local, 2013](https://www.thelocal.es/20131114/major-manufacturer-fagor-files-for-bankruptcy)). A study based on 44 interviews found that Fagor kept units losing millions of euros a year open well before the 2008 crisis. Its elected board often couldn't follow the business plans managers brought it, so it approved them and fired the managers when they failed ([Basterretxea, Cornforth and Heras-Saizarbitoria, 2022](https://journals.sagepub.com/doi/abs/10.1177/0143831X19899474), summarized by the authors in [GEO](https://geo.coop/articles/failure-cooperative-governance)). The Mondragon group had already put €70 million into Fagor from its shared fund, and other co-ops had lent more. In October 2013, when Fagor asked for €170 million more, the group's council refused unanimously, saying the money "would not ensure its business future" ([GEO, 2013](https://geo.coop/story/major-mondragon-co-operative-faltering)).
|
||||
|
||||
The risks that remain are the ones Fagor shows: big bets that members can't judge, losses that show up after the people who voted have left, and help from other co-ops that keeps a failing business going. So:
|
||||
|
||||
- **A second opinion on big commitments.** Some commitments need an independent written opinion before members vote: a single investment, a purchase of another business, or borrowing, including leases, guarantees and selling unpaid invoices, bigger than a quarter of the co-op's own capital (its reserve plus members' accounts) or $250,000, whichever is larger. Anything bigger than the co-op's whole capital needs one too, if it's over $50,000. So does buying any business for more than $350,000, the price above which the SBA requires an independent valuation before it backs a loan to buy a business ([SBA SOP 50 10 8.1](https://legacy.sba.gov/sites/default/files/2026-09/SOP%2050%2010%208.1%20Technical%20Policy%20Updates%20effective%2010.1.2026.docx)). The SBA rule protects a lender's price, not members' judgment, but it marks where a purchase is big enough to need an outside look. Related commitments within three years are added together, and the dollar figures rise with inflation. An outside opinion can easily cost thousands of dollars, so for commitments under $250,000 the federation's staff can give it for a flat fee, and the federation's fund pays that fee for co-ops with less than $250,000 of their own capital. The lock-holder picks the adviser from an approved list, and the co-op pays. A lender to the project can't give the opinion. The opinion comes with a one-page plain summary, including what the plan means for members' jobs, and there's a members' meeting to ask questions before the vote. It's advice, not a veto. The co-op publishes it with the result of the vote, and three years later reports how the project did against the plan. Replacing worn-out equipment like for like, and loans the bank has already checked, don't need it, except for buying a business. [Spin-offs](growth.md) get a similar opinion at a lower threshold.
|
||||
- **Losing parts of the business are visible.** Where a co-op has separate lines of business, each one reports its own results to members every year ([Does Sharing the Profit Mean Less Effort?](free-riding.md#small-teams-open-books)). A line losing money for years can't hide inside the total.
|
||||
- **A board that can follow the plans.** The federation offers training in reading the accounts, on paid time and open to every member, not just the board. The standard bylaws let members elect outside directors, from other co-ops or with business experience, but never more than a quarter of the board. The members choose them and can remove them.
|
||||
- **Limits on propping up a firm, decided in advance.** The [federation's fund](federation.md) helps a co-op through a bad year. Help to keep a firm going comes with a recovery plan and a limit set before any crisis, and cuts to members' pay or jobs in that plan need the members' vote. Help for the *workers*, like finding them jobs in other co-ops if the firm closes, has no such limit. Mondragon kept supporting Fagor and then said no. It's better to agree on where the line is before anyone's firm is on the wrong side of it.
|
||||
|
||||
## What's still open
|
||||
|
||||
- **Upkeep.** Nothing forces a co-op to maintain what it has. The warning light shows when upkeep falls behind, but members can still ignore it.
|
||||
- **Late failures.** A big project that fails after the members who chose it have left is paid for by the people who came after. The second opinion and the three-year report make bad bets less likely and more visible. They don't move the cost back to the people who made them.
|
||||
- **Deals with insiders after the loan.** The check on deals with insiders now outlasts the bank's loan as part of the lock ([Deals with insiders](the-lock.md#deals-with-insiders)). It's weaker once the loan is repaid, since the bank can no longer call it.
|
||||
- **Whether 30% is enough.** Italy requires 30%. Mondragon found 20% too low and moved to 50%. This model uses 30% as a minimum that each co-op can raise. [The pilot](pilot-spec.md#what-gets-measured) measures investment per worker, and the return it earns, against matched conventional firms. So if co-ops invest too little, or waste what they invest, it will show.
|
||||
- **Is the account rate right?** Two points above Treasury is well below what investors expect from a small business, so members may still prefer cash. Paying the co-op's own return on its assets instead would reward good investment directly, but it would also make the account more volatile. And lower-income members, who can take their whole share as cash, don't get the two points at all. That's fair only if the two points are a fair price for the risk the account carries, not a bonus. Which is better is unsettled.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: The Test
|
||||
section: Growing the Sector
|
||||
order: 60
|
||||
summary: The bank runs as an experiment: metrics set in advance, a fixed review date, and a scale-up or wind-down rule nobody can change after seeing the data.
|
||||
---
|
||||
|
||||
# The Test
|
||||
|
||||
The bank is an experiment, so it runs like one.
|
||||
|
||||
- **Pre-register the metrics before the first loan:** survival rates, productivity, wages, member retention, and loan performance, measured against matched conventional firms in the same sectors and regions.
|
||||
- **Fixed review point.** After a set period (long enough to see business cycles), an independent review publishes the results. The pilot sets it at year 12.
|
||||
- **Scale-up or wind-down, decided in advance.** If co-ops match or beat conventional firms at market-rate lending, the bank's capital is expanded. If they don't, it's wound down and the loan book is sold. Both outcomes are written into the charter from day one, so nobody can move the goalposts after seeing the data. That includes me.
|
||||
|
||||
[The Pilot, in Numbers](pilot-spec.md) spells all of this out: the exact measures, the comparison group, the pass and fail thresholds, and an emergency stop.
|
||||
|
||||
**What the test can prove, and what it can't.** At pilot scale, this tests *business viability*: can co-ops compete, survive, and repay market-rate debt? It does **not** test the labor-market effect, meaning the idea that as co-ops grow, conventional firms have to treat workers better or lose them. That effect is real in principle, but it needs a co-op sector big enough to move wages at the margin. Mondragon employs tens of thousands of people and hasn't reshaped Spanish employment practice. The labor-market effect is the long-run payoff of success, not something the pilot can claim. [After a Pass: How Big, How Fast](scale.md) works through how big the sector would need to get, and how the effect could be tested in local markets once it grows. On public numbers, that local test could only see a large effect, so it may never run.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: The Thesis
|
||||
section: Foundations
|
||||
order: 10
|
||||
summary: Personal property is untouchable. Productive capital, a permanent claim on other people's work, is a different thing, and there should be firms where that claim never forms.
|
||||
---
|
||||
|
||||
# The Thesis
|
||||
|
||||
**The word "property" protects two very different things, and only one of them deserves that protection.**
|
||||
|
||||
There is *personal property*: your house, your car, your savings, your tools. The things you live in and use. These are untouchable. Nobody is collectivizing your couch.
|
||||
|
||||
Then there is *productive capital*: a perpetual, tradeable claim on the output of a thousand people you will never meet. This is not the same kind of right. It has been wearing the same word by accident, and the accident does political work for the people who benefit from it.
|
||||
|
||||
The dividing line: **it stops being a tool and becomes capital when it earns you a claim on other people's labor that you don't work alongside.** The plumber's van is not extracting from anyone. TPG's stake in Conservice is. (What that company would look like as a co-op is worked through in [What If Conservice Had Been a Co-op?](conservice.md).)
|
||||
|
||||
What follows from the thesis is not "therefore the state may take that capital." It is something narrower and harder to argue with: **therefore there should be firms where that claim never forms in the first place, and nothing in law or finance should stop people from building them.**
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: Co-ops or Their Founders?
|
||||
section: Growing the Sector
|
||||
order: 65
|
||||
summary: If the pilot fails, the first question will be whether co-ops failed or the people who started them had less going for them, so founders on both sides are measured from government records and the verdict has to say how much that explains.
|
||||
---
|
||||
|
||||
# Co-ops or Their Founders?
|
||||
|
||||
[The Founder Problem](founder-problem.md) says the lock costs a successful founder most of what they'd get from selling the business. So the strongest founders may go conventional, and the bank would be lending to the ones who were left. If the pilot then fails, nobody will know whether co-ops failed or the people who started them did.
|
||||
|
||||
The pilot can't stop that from happening. It can make sure the answer isn't a matter of opinion. This page is how.
|
||||
|
||||
## Why it's worth measuring
|
||||
|
||||
The worry isn't just theory.
|
||||
|
||||
- **In equal-sharing organizations, the best-paid people leave most.** In Uruguay, co-op members paid above their co-op's median were about 3.7 times as likely to quit as members paid below it, and the gap was smaller in co-ops that paid more unequally ([Burdín, *Economic Journal*, 2016](https://doi.org/10.1111/ecoj.12272); [free copy](https://gwern.net/doc/economics/2016-burdin.pdf)). In Israel's kibbutzim, which shared income equally, members with more education and more skilled jobs were the most likely to leave during a financial crisis in the late 1980s ([Abramitzky, *Quarterly Journal of Economics*, 2008](https://doi.org/10.1162/qjec.2008.123.3.1111); [free copy](https://ranabr.people.stanford.edu/sites/g/files/sbiybj26066/files/media/file/abramitzky_qje_2008.pdf)). If talented people walk out of equal-sharing firms, some will also never walk in.
|
||||
- **Founders who believe in it stay.** The same Uruguayan study found founding members were less likely to quit, and the best-paid founders much less so than other best-paid members. That's the kind of founder [the founder page](founder-problem.md#the-honest-bottom-line) is betting on.
|
||||
- **A founder's background predicts how the business does.** Using Census records that link founders to their past jobs, one study found that founders with three or more years in their start-up's industry were about twice as likely to build one of the very fastest-growing firms (the top 1 in 1,000) as founders with no experience in it ([Azoulay, Jones, Kim and Miranda, NBER working paper 2018, published 2020](https://www.nber.org/papers/w24489)).
|
||||
- **That background isn't handed out fairly.** A study of why Black-owned businesses did worse than white-owned ones found that a large part of the gap came from Black owners having had less chance to work in a family business, or in a business like the one they started, first ([Fairlie and Robb, 2004](https://docs.iza.org/dp1292.pdf)). So "less experience" often means "fewer chances," not "less able." That shapes how the results are worded below.
|
||||
|
||||
It could also go the other way. The bank screens every plan, and co-ops always have at least three founders, so co-op founders could turn out to have *more* going for them than the firms they're compared with. Then a pass would flatter co-ops. The measurement below catches either.
|
||||
|
||||
## Who counts as a founder
|
||||
|
||||
The same rule on both sides: the co-ops and the matched conventional firms that got SBA loans ([Who co-ops are compared against](pilot-spec.md#who-co-ops-are-compared-against)).
|
||||
|
||||
- **A founder is someone who owned part of the business at the start and did real work in it.** For a co-op, that's a founding member named in the loan application. For an SBA firm, it's an owner of 20% or more, since those are the owners the SBA names on the loan. On both sides, they count only if payroll records show them working at least a set number of hours in the first two years. Someone added to the application for their résumé, who never works there, doesn't count, and the report lists how many such people there were.
|
||||
- **The two sides aren't the same in one way.** SBA owners personally guarantee the loan. Co-op members don't ([The Pilot, in Numbers](pilot-spec.md#loan-terms)). So the founders on the two sides didn't carry the same risk, and the report says so.
|
||||
- **Team size is handled openly.** Co-ops have three or more founders; most SBA firms have one or two owners at 20%. So the team's average is weighted by hours worked, and the "strongest founder" is compared only between teams of the same size, because the best of five people will usually beat the best of two just by numbers. The report says how many co-ops found matches at each size. SBA firms with nobody at 20% are counted and reported, not quietly dropped. As a second version, the analysis also counts the officers who run each SBA firm.
|
||||
|
||||
## What gets measured
|
||||
|
||||
Each item comes from government records, so nobody can talk their way up or down.
|
||||
|
||||
- **Past pay.** Pay per quarter actually worked, averaged over the best three of the eight years before founding. Using quarters worked and the best years means a parent who took time out for caregiving isn't marked down for the years away. Missing records count as missing, not as zero. It's ranked two ways, and both are reported:
|
||||
- **against everyone of the same age nationally,** which shows whether the co-ops drew people from the lower-paid end of the whole economy;
|
||||
- **against people of the same age, state and industry,** which shows whether they drew the lower-paid people *within* their line of work. Co-ops in lower-paid work are part of the design ([Who the Bank Lends To](who-gets-loans.md)), and a home care founder shouldn't count as having less going for them for earning less than an engineer.
|
||||
- **Experience in the industry.** Years worked in the co-op's industry before founding.
|
||||
- **Owned a business before.** From tax records of past business income.
|
||||
- **Age**, as a background fact, not as a score.
|
||||
|
||||
The report also shows the co-ops' industry mix against all new firms, since staying out of whole industries is one way strong founders could avoid co-ops without it showing in any one founder's record.
|
||||
|
||||
## Getting the data
|
||||
|
||||
**The charter sets it up before the first loan.** It directs the bank, the SBA, the IRS and the Census Bureau to set up the linking, with every approval in hand before lending starts. That matters because research on tax records needs IRS approval project by project, and the Census Bureau's researchers can't count on getting it. Past studies show it can be done: Brown and Earle linked SBA loans to Census business records ([*Journal of Finance*, 2017](https://papers.ssrn.com/abstract=2641184)), and Azoulay and his coauthors linked founders to their past jobs.
|
||||
|
||||
- **Co-op founders consent when they apply,** on the standard IRS form that lets the reviewers see their tax records. So the co-op side can be done even if the Census linking falls through. The records are linked after the loan decision and never go to the people deciding loans.
|
||||
- **Comparison founders aren't contacted.** Their records are linked inside the Census Bureau's secure research labs, where names and Social Security numbers are replaced with internal codes. Census law allows the data to be used only for statistics, never against the people in it, including for tax or immigration enforcement. Only totals leave the lab, after the Census Bureau's own privacy review.
|
||||
- **State wage records are voluntary.** Years of experience come partly from state unemployment-insurance records, and each state decides whether to share them. Coverage is reported state by state, and results are shown using only the states that took part.
|
||||
- **The cost comes from the pilot's budget**, so it counts against the bank in the money test.
|
||||
|
||||
**If the data never arrives,** the verdict says so in one fixed sentence that names which approval was missing and who withheld it. A missing founder analysis never delays or changes the pass or fail.
|
||||
|
||||
## How it's used
|
||||
|
||||
The analysis is filed in advance, with its code, the same as the rest of [the test](pilot-spec.md#what-gets-measured). It has three steps.
|
||||
|
||||
**1. Who came.** The report shows how co-op founders compare with the comparison founders on each measure, with both pay rankings. It shows the same comparison with first-time owners without much wealth set aside, since the bank seeks them out on purpose. And it shows how the founder measures differ by race, gender and whether founders are recent immigrants, so that a gap caused by who gets paid less for the same work is visible as that.
|
||||
|
||||
**2. How much of the result do founders explain?** The survival comparison is run a second time, also matching on the founders' measures. It isn't a second pass/fail test. The report gives **how far the result moved**, with its range of uncertainty. If co-ops were 30% more likely to fail in the main comparison and 10% more likely against firms with similar founders, founders explain about two-thirds of the gap. If the range around that change is too wide to tell whether it's zero or the whole gap, the report says "too few to tell," and that's decided by a rule filed in advance, not by the reviewers' judgment.
|
||||
|
||||
**3. How big a hidden difference would it take?** Records don't capture everything about a founder. So the report also says how strong a difference the records *can't* see would have to be to change the verdict, using a standard method for this question (the "E-value" of [VanderWeele and Ding, *Annals of Internal Medicine*, 2017](https://doi.org/10.7326/M16-2607)), measured against the pilot's actual pass line, not against "no difference at all." What counts as fragile is fixed in advance: a result is fragile if a hidden difference no stronger than the measured ones already turned out to be would change it.
|
||||
|
||||
## The verdict still stands
|
||||
|
||||
**The founder analysis doesn't change pass or fail.** The decision still rests on the [main comparison](pilot-spec.md#the-decision-rule).
|
||||
|
||||
That's on purpose. If the lock keeps the strongest founders away, that's part of what the lock *does*. It isn't an excuse for the co-ops, and a bigger program would face it too. And a second, adjusted test that could turn a fail into a pass would be the easiest way to rescue a failing pilot.
|
||||
|
||||
What changes is what the verdict says. It has to use one of these sentences, fixed in advance, and the fast-track bill after a pass has to print the whole sentence:
|
||||
|
||||
| Main comparison | Once founders are matched | The verdict says |
|
||||
| --- | --- | --- |
|
||||
| Passes | The gap barely moves | Passed. Founders' backgrounds don't explain it. |
|
||||
| Passes | Co-ops fall behind | Passed, but co-op founders had more past pay or experience than the comparison firms'. Co-ops with similar founders haven't been shown to hold their own. |
|
||||
| Fails | Most of the gap closes | Failed. Under this lock, co-ops drew founders with less past pay or industry experience, and that accounts for most of the gap. That's part of the result, not an excuse for it. |
|
||||
| Fails | The gap barely moves | Failed, and founders' backgrounds don't explain it. |
|
||||
| Either | Too few to tell | The main result, and "whether founders explain it couldn't be told." |
|
||||
|
||||
**A fail is a fail in every row.** The shutdown happens by itself. Nothing on this page is grounds to restart the bank. Trying again would take a new law and a new pilot held to the same decision rule. What the wording does is keep anyone from quoting one row as another: "co-ops don't work" and "co-ops under this lock don't attract founders with experience" are different findings, and a future design would need to fix different things.
|
||||
|
||||
## Watching the founders who stay
|
||||
|
||||
Founders who leave early can also sink a co-op. The pilot already publishes how often higher-paid members leave ([Who Sets the Pay](pay.md)). It now also publishes:
|
||||
|
||||
- **how often founders leave** in each of the first five years, against owners of the comparison firms who leave *without* a sale, since co-op founders can't sell their way out. A founder whose hours fall below the set level counts as having left, even if still on the books;
|
||||
- **how often the best-paid members leave**, with the comparison firms' best-paid employees as the yardstick;
|
||||
- **what founders who leave earn next,** against both the comparison owners and their own pay before founding. Earning more after a co-op could mean the brain drain the Uruguayan study found, or skills learned there. The report can't tell those apart, and says so.
|
||||
|
||||
The yearly audit of rejected applications ([Making It Last](durability.md#rigging-the-result)) also checks whether founders' past pay predicts approval once the business plan's numbers are held fixed. A bank trying to make step 1 look good would show up there.
|
||||
|
||||
## Asking the ones who didn't come
|
||||
|
||||
Records only cover founders who applied. The ones who went conventional, and the workers who never started anything, are invisible to the bank. So, once, in year 3, a federal statistical agency, not the bank, runs a voluntary survey with confidential answers. It samples new SBA borrowers in the same industries and workers in those industries who haven't started a business, in several languages.
|
||||
|
||||
- **Both deals are described side by side,** including the SBA's personal guarantee and the co-op's smaller downside as well as the lock.
|
||||
- **The wording is written by the reviewers, filed at year 0 and tested in advance.** Each person is randomly shown one of two framings, and both results are published, so the wording's effect is visible instead of hidden.
|
||||
- **The response rate is published,** with who didn't answer. No names go to the bank.
|
||||
|
||||
A survey answer isn't a decision made with real money, and people say things they wouldn't do. So it's published as background, and nothing in the verdict depends on it.
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **It measures the founder problem. It doesn't fix it.** If the analysis shows the strongest founders stayed away, the pilot has an answer, not a cure. The cure is in the founder deal ([The Founder Problem](founder-problem.md#the-tools)), and it may not be enough.
|
||||
- **Records measure chances as much as ability.** Past pay reflects discrimination, bargaining power and who you know, not only skill. The report breaks the founder gap down by race, gender and immigrant status and uses careful wording, but "less past pay" is still partly "fewer chances."
|
||||
- **Records miss people.** Recent immigrants may have little US record. Cash work and some self-employment never show up. The report says how many founders on each side couldn't be matched and shows them separately.
|
||||
- **It's a lot of statistics on a small sample.** With about 400 co-ops, "too few to tell" is a likely answer for step 2, and the page says so in advance rather than after.
|
||||
- **Worker groups muddy the comparison.** When the founders are ten workers from the same shop, the "strongest founder" is often a lead worker, not an entrepreneur. Those co-ops are reported separately.
|
||||
- **It can't see the founders who never considered co-ops.** The survey is the only window on them, and it's a weak one.
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: Who the Bank Lends To
|
||||
section: Growing the Sector
|
||||
order: 66
|
||||
summary: A bank that lends only to co-ops can still drift toward the safest ones, so part of the loan money is set aside for co-ops in lower-paid work, lending is published by group, and the final verdict has to say which kinds of co-ops it covers.
|
||||
---
|
||||
|
||||
# Who the Bank Lends To
|
||||
|
||||
The bank's charter says it lends only to worker co-ops. That stops it from turning into an ordinary bank. It doesn't stop a quieter kind of drift: lending mostly to the co-ops that were always the safest bets, like consultancies and software shops started by people with savings, and seldom to the home care, cleaning and food co-ops where many co-op members work today.
|
||||
|
||||
That drift matters for two reasons. It would make the test less useful, because a pass earned by consultancies says little about the work most people do. And it's the drift the founding plan predicts. A public lender judged on its losses has every reason to fund the safe bets. The plan calls that "risk-aversion under political accountability."
|
||||
|
||||
## What we know
|
||||
|
||||
- **It has happened here.** The law that created America's co-op bank in 1978 told its board to use its "best efforts" to keep at least 35% of its loans going to co-ops of low-income people. Nothing happened when it missed, and the bank drifted toward borrowers that could have borrowed anywhere ([Has This Been Tried?](precedents.md#americas-co-op-bank-the-national-consumer-cooperative-bank)). Lending to safer borrowers may also be part of why it survived. That's the trade this page has to manage.
|
||||
- **It isn't automatic.** A study of rated microfinance lenders in many countries over 11 years found that, across the industry, average loan size didn't grow and lending didn't shift toward cities. But lenders whose costs rose tended to make bigger loans, because small loans cost more to make per dollar lent ([Mersland and Strøm, *World Development*, 2010](https://doi.org/10.1016/j.worlddev.2009.05.006); [free copy](https://zenodo.org/records/16910149)). The pressure to drift is real, and it comes through costs.
|
||||
- **The sector may already be moving.** The 2025 survey of US worker co-ops found that the fastest-growing industry was business consulting ([US Federation of Worker Cooperatives, 2025](https://www.usworker.coop/blog/the-2025-state-of-the-sector-report-is-here/)). Only 242 co-ops answered the survey ([survey page](https://www.usworker.coop/census/)), so that's a signal, not a measurement.
|
||||
- **Enforced floors exist.** To stay certified, a US community development lender has to direct at least 60% of both the number and the dollar amount of its loans to the low-income places or people it's certified to serve. It's checked every year from loan-by-loan data, with no rounding: 59.9% fails. A lender that misses in one year can still pass on its three-year record ([CDFI Fund](https://www.cdfifund.gov/node/1030631); [Dechert, 2024](https://www.dechert.com/knowledge/onpoint/2024/3/department-of-the-treasury-s-community-development-financial-ins0.html)). Those lenders exist to serve that market, though. This bank exists to run a test, so it copies the idea of a checked, published share, not the 60%.
|
||||
|
||||
## Two groups, fixed in advance
|
||||
|
||||
Before the first loan, the pilot's filed plan defines two groups the results are reported on. The reviewers, not the bank, decide which group a co-op falls in when it applies.
|
||||
|
||||
- **Co-ops in lower-paid work.** The co-op's industry has to pass two checks. Nationally, its average pay is below the average for all private-sector jobs ($75,850 in 2024). In the co-op's county, its average pay is below the county's average for all private-sector jobs. The figures come from the federal count of employment and wages (the [Quarterly Census of Employment and Wages](https://www.bls.gov/cew/downloadable-data-files.htm)), which covers nearly every employer, rural counties included, at the finest industry level it publishes. The details:
|
||||
- **Why two checks.** I ran the county check alone on the 2024 figures. In the richest counties, like Santa Clara, San Francisco and Manhattan, the county average is so high that management consulting, accounting, architecture and engineering all count as "lower-paid" there. Those are the co-ops the set-aside exists to look past. The national check closes that. Care, cleaning, restaurants, child care and landscaping pass either way, in counties holding over 90% of those businesses. Consulting, law, software and engineering pass nowhere.
|
||||
- **Why averages.** Nobody publishes a typical (median) wage for one industry in one county, so averages are what can be checked. A few high earners pull an average up, which makes the test generous: about 55% of private-sector jobs are in industries that pass both checks where they are. The figures are pay per job per year, not per hour, so long-hours work looks better paid than it is, and part-time work looks worse paid.
|
||||
- **A lower tier.** Industries whose national average is below two-thirds of the all-jobs average (about $50,500 in 2024) form a lower tier, about 40% of private-sector jobs. Care, cleaning, restaurants and child care are in it; dentists' offices, trucking and remodeling aren't. **Half the set-aside, $45 million, is held for the lower tier,** under the same rules as the rest. Without that, the bank could fill the whole set-aside from the best-paid end of the group.
|
||||
- **When the county figure is withheld.** About a fifth of businesses are in county-and-industry figures the government withholds for privacy, and in counties with under 10,000 private-sector jobs it's about 70%. Then the state's figure for the industry is compared with the state's average for all jobs, not with the county's, because a state figure carries city pay that a rural county average doesn't. In my test, that lets 89% of small-county businesses in nationally lower-paid industries pass, against 66% when the state figure was set against the county average. The state figure is withheld for about 1% of businesses; then only the national check applies. The report says how often each fallback was used, by count and by dollars lent.
|
||||
- **Which industry.** The reviewers decide it from what the co-op actually sells: its revenue, its contracts and its members' jobs. The code it registers under for state payroll taxes is only a cross-check, since businesses largely describe themselves to get it. Staffing and "all other" catch-all codes don't qualify on their own; the co-op's real work has to. The reviewers publish the reason for each placement.
|
||||
- **Members' own pay is a ceiling.** Even in a qualifying industry, the co-op's planned median hourly pay for members, counting profit shares, has to be below the median hourly wage for all jobs in its area ([BLS area wage figures](https://www.bls.gov/oes/tables.htm)). After each of its first three years, the reviewers check it against real payroll. Pay is only a way out, never a way in: a co-op outside a lower-paid industry can't qualify by paying itself little.
|
||||
- **Which county and year.** The county where most of the co-op's hours are worked, by work location on its payroll records, not where members live. The figures are the average of the last three yearly releases, fixed at the start of each lending year, so nobody can time an application around a release.
|
||||
- **What moves a co-op.** The group is fixed for the loan unless the co-op misstated its business, or in its first three years most of its revenue moves into work that doesn't qualify, or its members' pay goes over the ceiling. Then its loan is moved to its real group in the published results, and the results are shown both ways. The loan itself isn't penalized. Related co-ops (shared members or control, or much of their revenue from each other) are placed as one family, so a firm can't split into a lower-paid co-op that borrows and a better-paid sister that uses what it buys.
|
||||
- **First-time owners without much wealth.** Most founding members, counted by their hours of work, have never owned a business, and their household wealth is below the median for people their age in the Federal Reserve's household wealth survey. Founders report their wealth in bands, sign it under penalty for false statements, and it's checked against their tax records with their consent. Founders count only if they work at least a set number of hours in the co-op's first year.
|
||||
|
||||
A co-op can be in both groups, one or neither.
|
||||
|
||||
## Money set aside
|
||||
|
||||
**About a third of the loan money, $90 million, is set aside for co-ops in lower-paid work.** Other co-ops can't borrow it. Nobody waits in line. Co-ops outside the group borrow from the rest of the money, the same as before. Half of it, $45 million, is held for the [lower tier](#two-groups-fixed-in-advance). That's a first guess, sized so the set-aside could fund a large share of the co-ops at the smaller loans this kind of work usually needs.
|
||||
|
||||
**The standards don't change.** Each loan is still judged on its own numbers by the same filed standards ([Guarding the Pilot](pilot-safeguards.md)), and the limits on any one industry and county still apply. If the set-aside and the 25% cap on one industry ever clash, the cap wins.
|
||||
|
||||
**Unused money goes back, but only after a check.** At the end of year 4, lower-tier money not yet lent moves to the rest of the set-aside, and set-aside money not yet lent goes back to the general pool, each only if the reviewers certify two things for the co-ops it was held for:
|
||||
|
||||
1. The bank decided applications from lower-paid co-ops as fast as others, measured from a complete application, within a time limit filed in advance.
|
||||
2. The audit of rejected lower-paid applications found that the bank applied its own standards ([the line](durability.md#when-rejections-mean-not-tested)).
|
||||
|
||||
If they can't certify both, the money stays set aside until the lending window closes. The verdict for lower-paid work is then **"not tested,"** not "failed," unless the reviewers show that good applications really were scarce.
|
||||
|
||||
**A shortfall with a clean audit is a finding.** If good applications from lower-paid work don't come even when the money is there and the bank handled them fairly, that's evidence about where co-ops can work. It's reported that way, the same rule the pilot uses when applicants are scarce ([The Pilot, in Numbers](pilot-spec.md#why-about-400-co-ops)).
|
||||
|
||||
**What counts.** Only new money to a co-op counts, and loans to related co-ops are added together, so splitting one loan into pieces can't pad the numbers. If the reviewers later find a co-op was put in the wrong group, its loan is moved to its real group, and the set-aside figures are recalculated. A co-op that misstated its industry on purpose is treated like any other false statement to the bank, under the rules against sham co-ops.
|
||||
|
||||
This isn't a lending target. Nobody at the bank is paid or judged on how much it lends, to any group.
|
||||
|
||||
## Priced like any other loan
|
||||
|
||||
Market rate means the whole cost of the loan, including the cost of making and servicing it, as a conventional lender would charge ([What "market rate" means](pilot-spec.md#what-market-rate-means)). Small loans cost more to make per dollar, so they carry a higher rate or a fee, as they would anywhere. The yearly rate audit checks that too. The bank can match repayments to a co-op's cash flow, with longer terms or a slow start, as long as the price for the risk stays the same.
|
||||
|
||||
So the set-aside isn't a subsidy, and it can't excuse a result. The money test is judged on the whole loan book, with no adjustment for the set-aside, whichever way it comes out.
|
||||
|
||||
## Published by group
|
||||
|
||||
Every year the bank publishes, for each group: applications, approvals, rejections, offers the co-op turned down, withdrawals, rates, collateral, loan sizes, time to a decision, and how the loans are doing. It also publishes the same figures for the lower tier, by where the co-op's industry falls on its county's wage scale (in fifths), by industry, by rural or urban county (using the federal government's standard rural codes), and by the founders' wealth band.
|
||||
|
||||
To protect small businesses, every published table needs at least 10 co-ops in each cell, counting every combination of details shown. Whether individual loans are behind on payments is published only in totals. Loan-by-loan data goes only to vetted researchers under a data-use agreement.
|
||||
|
||||
## Results by group
|
||||
|
||||
The pilot still passes or fails on the whole portfolio, using the two measures filed in advance ([The decision rule](pilot-spec.md#the-decision-rule)). Adding group results to the decision would make it easier to find one that looks good.
|
||||
|
||||
But the final report gives both measures, survival with fair pay and the bank's money, for each group. Because the groups are much smaller than the whole pilot, the plan files in advance the smallest difference each group could reliably show. For groups of a couple of hundred co-ops, "too few to tell" is the likely answer. When it is, the report says so, with the range of possible results and no single headline number.
|
||||
|
||||
Differences between groups are claimed only if a test of the difference itself, filed in advance, finds one. The verdict says which kinds of co-ops it covers. "Passed, with too few lower-paid co-ops to tell" is a different result from "passed across the board," and nobody gets to quote the first as the second.
|
||||
|
||||
The reviewers, not the bank, work out the bank's return for each group, using a method filed in advance for splitting shared costs. They report it with and without shared overhead, so the split can't be rigged to make one group look expensive.
|
||||
|
||||
If the pilot passes, any larger program's lending to lower-paid work is sized from that group's own result. If that result is "too few to tell," that lending can't grow faster than the rest ([After a Pass](scale.md)).
|
||||
|
||||
## Treated the same
|
||||
|
||||
The yearly audit of rejected applications ([Making It Last](durability.md#rigging-the-result)) now also looks at the two groups, in three ways.
|
||||
|
||||
- **Same numbers, same answer?** It compares approvals, rates and collateral across the groups for applications with similar numbers, and checks samples of approvals in both groups against the filed standards.
|
||||
- **Are the numbers themselves fair?** It checks each part of the bank's scoring, like collateral, credit history and founders' own money, to see whether it counts against a group more than it predicts repayment. Then it publishes whether a different scoring would predict repayment as well with less of that effect.
|
||||
- **Is the bar too high?** If approved co-ops in one group repay better than the rest, the bank was demanding too much of that group. The lottery among borderline applicants ([The Pilot, in Numbers](pilot-spec.md#who-co-ops-are-compared-against)) is the cleanest place to check.
|
||||
|
||||
The audit also reports by the founders' gender and race, as the pilot already does, collected separately from the loan file so underwriters don't see it. Answering is optional and changes nothing about the loan.
|
||||
|
||||
**Asking needs its own legal basis.** The fair-lending law forbids discriminating, not asking ([15 U.S.C. § 1691](https://www.law.cornell.edu/uscode/text/15/1691)). The ban on asking an applicant's race, color, religion, national origin or sex is in the rules written under it, with a few exceptions ([12 C.F.R. § 1002.5](https://www.law.cornell.edu/cfr/text/12/1002.5)). The law covers government lenders too ([§ 1691a](https://www.law.cornell.edu/uscode/text/15/1691a)), so the bank is bound by both. None of the ready-made exceptions fits it:
|
||||
|
||||
- **The federal small-business lending data rule** requires lenders to ask, behind a wall that keeps the answers from anyone deciding on the loan ([§ 1002.108](https://www.ecfr.gov/current/title-12/part-1002/section-1002.108)). Since a May 2026 change, it covers only lenders that make at least 1,000 small-business loans in each of two years, starting in 2028 ([§ 1002.105(b)](https://www.ecfr.gov/current/title-12/part-1002/section-1002.105), [§ 1002.114](https://www.ecfr.gov/current/title-12/part-1002/section-1002.114)). The pilot's bank makes far fewer than 1,000 loans a year. Collecting early is allowed only for lenders that expect to be covered, and continuing is allowed only for lenders that used to report. The rule is also being reconsidered again.
|
||||
- **A lender's own fair-lending self-test** can ask, if it tells applicants that answering is optional, why it's asking, and that the law forbids using the answer or a refusal against them ([§ 1002.5(b)(1)](https://www.law.cornell.edu/cfr/text/12/1002.5), [§ 1002.15](https://www.ecfr.gov/current/title-12/part-1002/section-1002.15)). But collection the government requires doesn't count as a voluntary self-test, and the pilot requires these figures.
|
||||
- **Asking later, through the reviewers,** doesn't escape the rule: it covers "every aspect of an applicant's dealings with a creditor" about a loan ([§ 1002.2(m)](https://www.ecfr.gov/current/title-12/part-1002/section-1002.2)). And applicants who were turned down rarely answer a later survey, which would lose the very people the audit is for.
|
||||
|
||||
So the bank's law uses the route the rules already leave open: information "required by a regulation" to monitor compliance ([§ 1002.5(a)(2)](https://www.law.cornell.edu/cfr/text/12/1002.5)). The law directs the bank's federal regulator to issue that rule, using the categories the small-business data rule uses (ethnicity, race and sex of each principal owner), so the results can be compared with national figures if those are ever collected. The rule copies the self-test notice to applicants and goes further than the data rule's wall. That rule lets a lender skip the wall, with notice, when keeping it isn't feasible, and a bank with a few loan officers would. So the bank never holds the answers at all: a separate custodian collects them under coded IDs and matches them to loan files only after decisions are final. As a fallback, the law authorizes the question directly, "notwithstanding" the rules. A deal with a single agency wouldn't do: it would last only as long as that agency's goodwill. The bank asks nothing about birthplace or language.
|
||||
|
||||
- **Kept apart, kept briefly.** They're used only for the audit and the pilot's published results, never shared with law enforcement or immigration authorities, and kept out of public-records requests. Each answer is deleted three years after the audit that used it, the data rule's own retention period ([§ 1002.111](https://www.ecfr.gov/current/title-12/part-1002/section-1002.111)). Results for fewer than ten people aren't published, and refusal rates are.
|
||||
- **Publishing has a price, paid on purpose.** Published results lose the protection the self-test rule gives a lender's own findings, and anyone suing the bank can use them. That's the point of an audit the public can check. The test a replacement scoring rule has to pass is published in advance, so turning one down is a rule being followed, not a judgment call.
|
||||
- **If Congress won't include it,** estimates from surname and address, the method the Consumer Financial Protection Bureau uses, can stand in for totals. They say nothing about sex, work poorly for small groups and for co-ops with several founders, and can't be checked by the people they describe. A better fallback is matching founders, with their consent, to their own answers on the census, inside the Census Bureau's secure research program, so the bank never holds a race file. The reviewers compute the surname-and-address estimates from the first year anyway, so if a later Congress repeals the question, there's a comparable series to continue.
|
||||
|
||||
Race and sex measure results; they never decide a loan. If the audit finds a scoring rule that works against a group, a replacement is adopted only if it predicts repayment as well or better for all applicants. That's the bank's own rule, not a claim about what the courts require, so it holds whatever happens to the law on unintended discrimination. Gaps by race are reported both as they are and after taking founders' wealth into account, so it shows how much of any gap runs through wealth and how much doesn't.
|
||||
|
||||
## What this doesn't solve
|
||||
|
||||
- **It can't make people apply.** The set-aside makes money available. It can't make groups come forward. The [founding lottery](before-launch.md) and sponsors like unions and worker centers do that, and the pilot can only measure how well.
|
||||
- **The lower-paid group is wide.** It runs from the lowest-paid work to work just under the average, about 55% of private-sector jobs. The lower tier holds half the set-aside for the bottom of it, but inside each half the bank can still lend mostly at the top. The results are published by fifths of the local wage scale, so that shows.
|
||||
- **Market prices aren't neutral.** In home care and cleaning, prices are largely set by whoever buys the work, like Medicaid or a contractor, not by how well the business is run. A co-op that fails there may be failing the price, not the co-op form. Comparing co-ops with conventional firms in the same industry catches some of that, not all.
|
||||
- **Industry is a rough stand-in.** A co-op in a low-wage industry can still pay well; the pay ceiling catches that. The reverse isn't caught: a rural design or computer-repair co-op whose members earn little fails the national check, and borrows from the general pool on the same terms. Letting co-ops in on their own low pay would reward paying yourselves little at application, so it isn't allowed. A determined co-op can still describe its work to fit, and the reviewers' placement is a judgment. The pilot publishes members' actual pay too.
|
||||
- **Wealth is hard to check.** Tax records show income, not savings. Some founders will understate their wealth.
|
||||
- **The numbers are guesses.** $90 million, the half for the lower tier, its two-thirds line, three years of pay checks and year 4 are first guesses. The pilot publishes how much of the set-aside was used, so the next design can do better.
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Worker Ownership
|
||||
section: The Co-op
|
||||
order: 20
|
||||
summary: What a worker co-op means here: one member, one vote from day one, and a profit share that grows with tenure and then caps.
|
||||
---
|
||||
|
||||
# Worker Ownership
|
||||
|
||||
A worker co-op under this model means two things, and they are not the same thing:
|
||||
|
||||
**One worker, one vote. Unconditional, from membership.** This is the moral core and it does not ramp, vest, or vary. The objection to the current system is *domination*: being ruled by a distant firm that answers to nobody who works there. The cure for domination is the vote, and you don't earn your way into not being dominated.
|
||||
|
||||
**Profit share ramps with tenure, then caps.** A new member starts at a partial share and grows to a full, equal share over roughly five to ten years. After the cap, everyone's share is identical. The thirty-year lifer and the eight-year veteran split profit the same.
|
||||
|
||||
Why the ramp: it kills the dilution panic. Under flat equal shares, every new hire immediately eats everyone's slice, and cooperatives empirically under-hire for exactly that reason. With a ramp, hiring decisions move toward "does this person grow the pie" during precisely the window when their value is most uncertain. The ramp only softens the problem, though. How co-ops can grow into lower-margin work without shrinking everyone's share is in [When Growing Means Sharing](growth.md).
|
||||
|
||||
Federal tax law narrows what the ramp can do. Profit shares are counted per hour worked, and the part of a share a new member hasn't reached yet can't go to the other members without being taxed twice. So by default it goes to the co-op's permanent reserve. The ramp still makes new members earn their way to a full share, but it no longer protects everyone else's share from new hires, unless a co-op pays the difference as wages for years of service ([How a Locked Co-op Is Taxed](taxes.md#the-tenure-ramp)).
|
||||
|
||||
Protected leave (parental, medical, caregiving) counts as tenure, so taking time off to care for someone doesn't set your share back. So does earlier time at the same co-op: someone who leaves and comes back within a few years picks up where they left off.
|
||||
|
||||
Membership itself comes automatically after a short probation, unless a worker opts out. In jobs like home care, where most people move on within a year or two, that's what keeps a co-op from becoming a club of the few who stay ([When People Don't Stay Long](high-turnover.md)).
|
||||
|
||||
Where does the profit share actually go? Part is paid in cash each year, and part goes into a personal account that pays each year's share back after about five years. See [Member Capital Accounts](capital-accounts.md).
|
||||
|
||||
Why the cap: uncapped tenure weighting builds a gerontocracy of shares and chains people to their first employer. Capped at a career-fraction, people can change companies, move cities, start over, and still reach a full share with plenty of career left.
|
||||
|
||||
- **Base salaries still exist**, set by the company. A brain surgeon won't work somewhere without a high salary, and shouldn't have to. Hard jobs and in-demand jobs still pay more. The incentive to do difficult work survives intact. Pay isn't capped, but raising salaries doesn't let members skip the co-op's reserve: it takes its share of pay above the going rate too ([Investing for the Long Run](the-long-run.md)). How pay gets set, in the open and by the right people, is in [Who Sets the Pay](pay.md).
|
||||
- **Governance is up to each company.** Workers pass bylaws deciding what requires a vote and what doesn't. Some will use direct democracy, some representative systems, some will delegate almost everything to management they hire and can fire. "Workers vote on every decision" is a strawman and always was. Why anyone works hard when the profit is split so many ways, and what happens when someone doesn't, is in [Does Sharing the Profit Mean Less Effort?](free-riding.md).
|
||||
|
||||
The concrete case: the average employee where I work generates roughly $150,000/year in profit after all costs. That money currently flows upward to private equity. A firm with those economics, built as a co-op, sends it to the people who made it. The numbers, using public figures, are in [What If Conservice Had Been a Co-op?](conservice.md).
|
||||
@@ -9,9 +9,18 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Markdig" Version="1.4.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Public /socialism documents. Underscore-prefixed files (founding draft, progress log) are private and never deployed. -->
|
||||
<Content Include="Content/Socialism/*.md" Exclude="Content/Socialism/_*.md">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Resources/**">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace JoshHeaps.Net.Models;
|
||||
|
||||
public class SocialismDoc
|
||||
{
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
public string Section { get; set; } = string.Empty;
|
||||
public int Order { get; set; }
|
||||
public string HtmlContent { get; set; } = string.Empty;
|
||||
public List<SocialismHeading> Headings { get; set; } = [];
|
||||
public int ReadingMinutes { get; set; }
|
||||
}
|
||||
|
||||
public record SocialismHeading(int Level, string Id, string Text);
|
||||
@@ -7,33 +7,68 @@
|
||||
|
||||
<div id="holdemApp">
|
||||
<header id="holdemHeader">
|
||||
@* Six equally-weighted buttons used to sit here in two rows, and on a 390px
|
||||
phone the felt did not start until 225px down — a quarter of the screen spent
|
||||
before the dealer saw the table. Only two of the six are per-hand controls.
|
||||
The rest have gone where they belong rather than into a "more" menu, which
|
||||
would have relocated the crowding rather than removed it: Reset Table down to
|
||||
the sidebar with the save/load pair — in a panel called Save & reset, so the
|
||||
danger is on the label rather than at the end of a scroll — Quick Split down
|
||||
with Common rulings where the other "open something else" link already lives,
|
||||
and the way back to the site up top where a back link goes. *@
|
||||
@* The word is its own element so a phone can drop it and keep the arrow, and the
|
||||
aria-label is what a screen reader gets either way. It contains the visible
|
||||
text on the sizes that still show it, which is what WCAG 2.5.3 asks of a name
|
||||
that says more than the label does. *@
|
||||
<div id="holdemHeaderLeft">
|
||||
<a href="/casino" class="holdemBackLink" aria-label="Back to Casino">←<span class="backLinkWord"> Casino</span></a>
|
||||
<h1>Texas Hold'em Dealer</h1>
|
||||
<p class="holdemSubtitle">Seat your players, track the action, split the pot.</p>
|
||||
<span id="handCounterBadge" class="handCounterBadge">Hand #1</span>
|
||||
</div>
|
||||
<div id="holdemHeaderActions">
|
||||
<a href="/casino/texasholdem/quicksplit" class="holdemBtn" title="Chop a pot without setting up a table">Quick Split</a>
|
||||
<button id="misdealBtn" class="holdemBtn" title="Take this hand back — every chip returns to the player who put it in, and the same button deals again" hidden>Misdeal</button>
|
||||
<button id="nextHandBtn" class="holdemBtn" title="Move the button, small blind and big blind to the next occupied seat">Next Hand ⟳</button>
|
||||
<button id="resolvePotBtn" class="holdemBtn holdemBtnAccent">Resolve Pot</button>
|
||||
<button id="resetTableBtn" class="holdemBtn holdemBtnDanger">Reset Table</button>
|
||||
<a href="/casino" class="holdemBtn holdemBtnGhost">← Casino</a>
|
||||
@* Not the accent any more. It is a shortcut to a sheet, available all night,
|
||||
and it is never the next step — the bar always has that, and at showdown
|
||||
the bar says so in as many words. See .actionAdvance in holdem.css. *@
|
||||
<button id="resolvePotBtn" class="holdemBtn">Resolve Pot</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@* The seat sheet was the only one of the tool's seven overlays with nothing behind
|
||||
it. On a phone it covers the felt from 250px down and the action bar with it, so
|
||||
it is modal in everything but appearance — and what stayed lit above it was the
|
||||
header, with Misdeal, Next Hand and Resolve Pot live under a thumb that was
|
||||
reaching for a name field. NN/g and Material both say the same thing about this:
|
||||
a modal sheet with an invisible scrim tells the user they can still touch what
|
||||
they can still see, and they can't.
|
||||
|
||||
Not an `*Overlay` id, deliberately — Modal watches those and would take the
|
||||
sidebar panel this becomes on a desktop for a dialog. It is driven entirely by
|
||||
body.seatSheetOpen, and it is `display: none` above 700px where the panel is
|
||||
just a panel. *@
|
||||
<div id="seatSheetScrim"></div>
|
||||
|
||||
<div id="holdemLayout">
|
||||
<section id="tableSection">
|
||||
<div id="boardStrip" hidden></div>
|
||||
<div id="pokerTableWrap">
|
||||
<div id="pokerTable">
|
||||
<div id="tableWatermark">Texas Hold'em</div>
|
||||
<div id="potBadge">
|
||||
<span id="potBadgeLabel">Pot</span>
|
||||
<span id="potBadgeAmount">$0</span>
|
||||
<div id="tableCentre">
|
||||
<div id="boardRow" hidden aria-label="Community cards"></div>
|
||||
<div id="potBadge">
|
||||
<span id="potBadgeLabel">Pot</span>
|
||||
<span id="potBadgeAmount">$0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="betsLayer" aria-hidden="true"></div>
|
||||
<div id="seatsLayer"></div>
|
||||
</div>
|
||||
<p id="tableHint" class="tableHint"></p>
|
||||
<p class="tableHintActions">
|
||||
<button type="button" id="openRulingsBtn" class="linkBtn">Common rulings</button>
|
||||
<a href="/casino/texasholdem/quicksplit" class="linkBtn" title="Chop a pot without setting up a table">Quick Split</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<aside id="holdemSidebar">
|
||||
@@ -48,8 +83,12 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<details class="sidebarPanel" open>
|
||||
<summary>Table Setup</summary>
|
||||
@* Not `open` in the markup any more. Whether this panel starts open is a
|
||||
question about the table, so main.js answers it after the state has
|
||||
loaded: open on a table nobody has set up yet, shut on one already
|
||||
half-way through a night. *@
|
||||
<details class="sidebarPanel" id="tableSetupPanel">
|
||||
<summary><h2>Table Setup</h2> <span class="panelGlance" id="glanceTableSetup" hidden></span></summary>
|
||||
<div class="panelBody">
|
||||
<label class="fieldRow">
|
||||
<span>Seats at table</span>
|
||||
@@ -68,56 +107,235 @@
|
||||
</label>
|
||||
<p class="fieldHint">Turn this on if a person outside the game deals — no seat shows the dealer button, but blinds still rotate automatically each hand.</p>
|
||||
<label class="fieldRow">
|
||||
<span>Base pot amount ($)</span>
|
||||
<input type="number" id="basePotInput" min="0" step="1" placeholder="e.g. 500" />
|
||||
<span>Ask me for the cards as they come out</span>
|
||||
<input type="checkbox" id="trackBoardToggle" />
|
||||
</label>
|
||||
<p class="fieldHint">Every player who didn't go all-in short is assumed to have put in this amount. Enter a smaller amount on a seat only if that player is all-in for less.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="sidebarPanel">
|
||||
<summary>Chip Legend</summary>
|
||||
<div class="panelBody">
|
||||
<div id="chipLegendList"></div>
|
||||
<button id="addChipColorBtn" class="holdemBtn holdemBtnSmall">+ Add chip color</button>
|
||||
<p class="fieldHint">Give each chip color a dollar value, then count chips per seat instead of typing a total.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="sidebarPanel">
|
||||
<summary>Table Notes</summary>
|
||||
<div class="panelBody">
|
||||
<textarea id="tableNotes" rows="4" placeholder="Anything else worth tracking — side bets, IOUs, running totals..."></textarea>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="sidebarPanel">
|
||||
<summary>Session Standings</summary>
|
||||
<div class="panelBody">
|
||||
<div id="standingsTable">
|
||||
<div class="standingsRow standingsHeader">
|
||||
<span>Player</span>
|
||||
<span>In</span>
|
||||
<span>Won</span>
|
||||
<span>Net</span>
|
||||
</div>
|
||||
<div id="standingsList"></div>
|
||||
@* The label used to lead with "Track the board", which is the feature's name
|
||||
rather than what pressing it does to your evening. What it does is put a
|
||||
card sheet in front of you at four moments: each street, and the showdown. *@
|
||||
<p class="fieldHint">Every time you deal a street, a sheet asks which cards came off the deck, and puts them on the felt. At the showdown it asks for each hand that turns over — then Resolve Pot reads the winner and the amount instead of asking you. Close the sheet at any point to go back to picking the winner yourself.</p>
|
||||
<label class="fieldRow">
|
||||
<span>Keep the screen on during a hand</span>
|
||||
<input type="checkbox" id="keepScreenAwakeToggle" />
|
||||
</label>
|
||||
@* On by default, and the only setting here that is. A dealer has to
|
||||
opt in to the tool doing more than they asked; they should not have
|
||||
to opt in to it staying readable. *@
|
||||
<p class="fieldHint">Stops the phone dimming and locking between the flop and the turn. It only holds while betting is open, so the screen sleeps as usual during breaks — and the tool says so the first time it takes hold.</p>
|
||||
<div class="blindsRow">
|
||||
<label class="fieldRow">
|
||||
<span>Small blind</span>
|
||||
<input type="number" id="smallBlindInput" min="0" step="0.5" />
|
||||
</label>
|
||||
<label class="fieldRow">
|
||||
<span>Big blind</span>
|
||||
<input type="number" id="bigBlindInput" min="0" step="0.5" />
|
||||
</label>
|
||||
</div>
|
||||
<label class="fieldRow">
|
||||
<span>Starting stack ($)</span>
|
||||
<input type="number" id="defaultStackInput" min="0" step="1" placeholder="e.g. 200" />
|
||||
</label>
|
||||
<p class="fieldHint">Blinds post themselves each hand. Seats without their own stack start here.</p>
|
||||
|
||||
<details class="subPanel">
|
||||
<summary>House take</summary>
|
||||
<div class="subPanelBody">
|
||||
<div class="blindsRow">
|
||||
<label class="fieldRow">
|
||||
<span>Rake (%)</span>
|
||||
<input type="number" id="rakePercentInput" min="0" max="100" step="0.5" placeholder="0" />
|
||||
</label>
|
||||
<label class="fieldRow">
|
||||
<span>Cap ($)</span>
|
||||
<input type="number" id="rakeCapInput" min="0" step="1" placeholder="none" />
|
||||
</label>
|
||||
</div>
|
||||
<label class="fieldRow">
|
||||
<span>No flop, no drop</span>
|
||||
<input type="checkbox" id="rakeNoFlopInput" />
|
||||
</label>
|
||||
<p class="fieldHint">
|
||||
Taken from the pot when it's awarded, and shown before anything is pushed.
|
||||
Rounded down to a chip the table has, so the players keep what the house
|
||||
can't make. Leave the percentage at zero for a game that doesn't rake.
|
||||
</p>
|
||||
<p class="fieldHint" id="rakeTakenHint" hidden></p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="subPanel">
|
||||
<summary>Without live betting</summary>
|
||||
<div class="subPanelBody">
|
||||
<label class="fieldRow">
|
||||
<span>Base pot amount ($)</span>
|
||||
<input type="number" id="basePotInput" min="0" step="1" placeholder="e.g. 500" />
|
||||
</label>
|
||||
<p class="fieldHint">For tracking a hand by hand instead of tapping the action: everyone is assumed to have put in this much, unless their seat says otherwise.</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@* These four were one panel called "Session", which is a container word rather
|
||||
than a name: it held the standings, a waiting list, the hand log and the
|
||||
save/load/reset buttons — a topic, a task, a log and three utilities, which
|
||||
is four kinds of thing under one label. It measured at 1206px and 103
|
||||
elements, and "The table itself" was 1052px down inside it, so reaching
|
||||
Reset table meant opening a panel called Session and scrolling past the
|
||||
money and the log to get there.
|
||||
|
||||
Each panel is named for the thing a dealer came looking for, and the
|
||||
subheads that used to do that job are gone, because the summary says it
|
||||
now. They are in the order of a night: set the table up, watch the money,
|
||||
fill the seats, look back at a hand — and, last and apart from the rest,
|
||||
the two buttons that end it. *@
|
||||
<details class="sidebarPanel">
|
||||
<summary><h2>Standings</h2> <span class="panelGlance" id="glanceStandings" hidden></span></summary>
|
||||
<div class="panelBody">
|
||||
<p id="storageWarning" class="storageWarning" hidden></p>
|
||||
<div class="standingsTableWrap">
|
||||
<div id="standingsTable">
|
||||
<div class="standingsRow standingsHeader">
|
||||
<span>Player</span>
|
||||
<span class="standingsInHead">In</span>
|
||||
<span class="standingsWonHead">Won</span>
|
||||
<span class="standingsNetHead">Net</span>
|
||||
<span class="standingsChipsHead" title="What they are holding — the cash to count out if they leave now">Chips</span>
|
||||
<span title="Those chips, less the cash they handed over">Up/down</span>
|
||||
</div>
|
||||
<div id="standingsList"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p id="standingsEmptyHint" class="fieldHint">Everyone who has bought in shows up here, with what they'd be owed if the night ended now.</p>
|
||||
|
||||
<div id="bankReconcile" class="bankReconcile" hidden>
|
||||
<div class="bankReconcileRow">
|
||||
<span>Cash taken</span>
|
||||
<span id="bankCashIn"></span>
|
||||
</div>
|
||||
<div class="bankReconcileRow">
|
||||
<span>Chips in play</span>
|
||||
<span id="bankChipsInPlay"></span>
|
||||
</div>
|
||||
<div class="bankReconcileRow" id="bankDiscardedRow" hidden>
|
||||
<span>Taken off the table</span>
|
||||
<span id="bankDiscarded"></span>
|
||||
</div>
|
||||
<div class="bankReconcileRow" id="bankRakedRow" hidden>
|
||||
<span>House take</span>
|
||||
<span id="bankRaked"></span>
|
||||
</div>
|
||||
<div class="bankReconcileRow" id="bankPaidOutRow" hidden>
|
||||
<span>Paid out</span>
|
||||
<span id="bankPaidOut"></span>
|
||||
</div>
|
||||
<p id="bankReconcileVerdict" class="bankReconcileVerdict"></p>
|
||||
<button type="button" id="bankRecordBuyInsBtn" class="holdemBtn holdemBtnSmall" hidden></button>
|
||||
</div>
|
||||
|
||||
<div id="settleUp" hidden>
|
||||
<h3 class="panelSubhead">Settle up</h3>
|
||||
<ul id="settleUpList" class="settleUpList"></ul>
|
||||
<p id="settleUpNote" class="fieldHint"></p>
|
||||
</div>
|
||||
<p id="standingsEmptyHint" class="fieldHint">Once you resolve a pot, everyone's running total for the night shows up here.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="sidebarPanel">
|
||||
<summary>Hand History</summary>
|
||||
<summary><h2>Waiting list</h2> <span class="panelGlance" id="glanceWaiting" hidden></span></summary>
|
||||
<div class="panelBody">
|
||||
<form id="waitingListForm" class="waitingAdd">
|
||||
<input type="text" id="waitingNameInput" maxlength="24" placeholder="Who's waiting?" autocomplete="off" />
|
||||
<button type="submit" id="waitingAddBtn" class="holdemBtn holdemBtnSmall">Add</button>
|
||||
</form>
|
||||
<ol id="waitingList" class="waitingList"></ol>
|
||||
<p id="waitingEmptyHint" class="fieldHint">Nobody waiting. Add the people standing behind the chairs, and the tool will offer them the next seat that opens.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="sidebarPanel">
|
||||
<summary><h2>Hands</h2> <span class="panelGlance" id="glanceHands" hidden></span></summary>
|
||||
<div class="panelBody">
|
||||
<div class="handHistoryFilterRow">
|
||||
<input id="handHistoryFilter" type="search" class="handHistoryFilter"
|
||||
placeholder="Find a hand — name, amount or #" aria-label="Filter the hand log by player, amount or hand number" />
|
||||
<span id="handHistoryCount" class="handHistoryCount" hidden></span>
|
||||
</div>
|
||||
<div id="handHistoryList"></div>
|
||||
<p id="handHistoryEmptyHint" class="fieldHint">Resolved pots show up here as you play, in case a chip count is disputed later.</p>
|
||||
<p id="handHistoryNoMatch" class="fieldHint" hidden></p>
|
||||
<p id="handHistoryEmptyHint" class="fieldHint">Resolved pots are logged here, in case a chip count is disputed later.</p>
|
||||
|
||||
<div class="handHistoryActions">
|
||||
<button id="exportHistoryBtn" class="holdemBtn holdemBtnSmall">Export CSV</button>
|
||||
<button id="clearHistoryBtn" class="holdemBtn holdemBtnSmall holdemBtnDanger">Clear history</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="sidebarPanel">
|
||||
<summary><h2>Chip Legend</h2> <span class="panelGlance" id="glanceChips" hidden></span></summary>
|
||||
<div class="panelBody">
|
||||
<div id="chipLegendList"></div>
|
||||
<button id="addChipColorBtn" class="holdemBtn holdemBtnSmall">+ Add chip color</button>
|
||||
<p class="fieldHint">Give each chip color a value, then count chips on a seat instead of typing a total.</p>
|
||||
|
||||
<div id="colourUp" hidden>
|
||||
<h3 class="panelSubhead">Stacks in chips</h3>
|
||||
<dl id="colourUpList" class="colourUpList"></dl>
|
||||
<p class="fieldHint">The fewest chips that make each stack — for colouring up, or counting one out.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="sidebarPanel" id="keyboardPanel">
|
||||
<summary><h2>Keyboard</h2></summary>
|
||||
<div class="panelBody">
|
||||
<dl id="shortcutLegend" class="shortcutLegend"></dl>
|
||||
<p class="fieldHint">Nothing fires while you're typing in a field, or while a sheet is open over the table.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="sidebarPanel">
|
||||
<summary><h2>Table Notes</h2> <span class="panelGlance" id="glanceNotes" hidden></span></summary>
|
||||
<div class="panelBody">
|
||||
<textarea id="tableNotes" rows="4" aria-label="Table notes" placeholder="Anything else worth tracking — side bets, IOUs, running totals..."></textarea>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@* Last, and on its own, because everything in it is about the night as a whole
|
||||
rather than the hand in front of you — and because two of the three buttons
|
||||
are the ones you cannot take back.
|
||||
|
||||
Reset table came down from the header a while ago, where it sat in the same
|
||||
weight as Resolve Pot: the most-pressed control in a session beside the one
|
||||
that throws the night away. It used to be buried at the bottom of Session,
|
||||
1052px down, which hid it but also meant nobody could find it on purpose.
|
||||
Naming the panel for both jobs puts the danger on the label, where it is
|
||||
read before the panel is opened rather than met at the end of a scroll. *@
|
||||
<details class="sidebarPanel">
|
||||
<summary><h2>Save & reset</h2></summary>
|
||||
<div class="panelBody">
|
||||
<p class="fieldHint">
|
||||
The night lives in this browser and nowhere else. Save it to a file to carry it
|
||||
to another device, or to have something to come back to if this one dies.
|
||||
</p>
|
||||
@* That sentence has always been true, and always been missing its
|
||||
second half: a browser is allowed to throw site data away when the
|
||||
device runs short, and Safari does it on a timer. So the panel now
|
||||
says which of the two situations this device is actually in, and
|
||||
offers the one thing that changes it. See requestDurable() in
|
||||
state.js for why the ask is a button rather than a page-load call. *@
|
||||
<p class="fieldHint" id="storageDurabilityHint" hidden></p>
|
||||
<button id="keepStorageBtn" class="holdemBtn holdemBtnSmall" hidden>Ask this browser to keep it</button>
|
||||
<div class="handHistoryActions">
|
||||
<button id="saveTableBtn" class="holdemBtn holdemBtnSmall">Save table file</button>
|
||||
<button id="loadTableBtn" class="holdemBtn holdemBtnSmall">Load table file</button>
|
||||
<input id="loadTableInput" type="file" accept="application/json,.json" hidden />
|
||||
<button id="resetTableBtn" class="holdemBtn holdemBtnSmall holdemBtnDanger">Reset table</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,15 +343,31 @@
|
||||
<template id="seatTemplate">
|
||||
<div class="seat" draggable="true">
|
||||
<div class="seatChairNumber"></div>
|
||||
<button class="dealerChip" title="Make this seat the dealer button">D</button>
|
||||
<button class="dealerChip" type="button" title="Make this seat the dealer button">D</button>
|
||||
<span class="blindBadge sbBadge">SB</span>
|
||||
<span class="blindBadge bbBadge">BB</span>
|
||||
<span class="blindBadge strBadge" title="Straddled — the action comes back to this seat">STR</span>
|
||||
<div class="seatPuckName"></div>
|
||||
<div class="seatPuckCards"></div>
|
||||
<div class="seatPuckTotal"></div>
|
||||
<div class="seatPuckStack"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@* Four things stacked flat used to be five: name, in/out, two hole-card slots, stack,
|
||||
a five-box chip counter and a list of custom fields, every one of them at the same
|
||||
weight, all of it open at once. Measured on a 390px phone the sheet came to 594px,
|
||||
and the three controls a dealer touches on nearly every visit — the name, the two
|
||||
status boxes, the stack — were 132px of it. The chip counter alone was 174px, the
|
||||
tallest thing in the sheet, for a job most tables do once a night if at all; the two
|
||||
card slots were 62px of dashed grey on every table that never asked to be shown a
|
||||
card.
|
||||
|
||||
So the flat list is now three rows and three disclosures, and which of the three
|
||||
stand open is a question about *this seat*, answered in renderSeatDisclosures: a
|
||||
seat holding cards opens its cards, a seat with chips counted opens its count, a
|
||||
seat with notes on it opens its notes. Anything with nothing in it stays a one-line
|
||||
summary that says so. Nothing was removed and nothing moved to another screen. *@
|
||||
<template id="seatEditorTemplate">
|
||||
<div class="seatEditorForm">
|
||||
<label class="fieldRow">
|
||||
@@ -150,34 +384,64 @@
|
||||
<span>Fold</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="seatCards">
|
||||
<input type="text" class="cardInput card1Input" maxlength="2" placeholder="__" />
|
||||
<input type="text" class="cardInput card2Input" maxlength="2" placeholder="__" />
|
||||
</div>
|
||||
<div class="seatContribution">
|
||||
<div class="chipCounts"></div>
|
||||
<label class="fieldRow amountOverrideRow">
|
||||
<span>$ amount</span>
|
||||
<input type="number" class="amountOverrideInput" min="0" step="1" placeholder="auto" />
|
||||
</label>
|
||||
<div class="seatTotal">Total: <span class="seatTotalAmount">$0</span></div>
|
||||
</div>
|
||||
<div class="customFields"></div>
|
||||
<button class="holdemBtn holdemBtnSmall addFieldBtn">+ field</button>
|
||||
<label class="fieldRow stackRow">
|
||||
<span>Stack</span>
|
||||
<input type="number" class="stackInput" min="0" step="1" placeholder="table default" />
|
||||
</label>
|
||||
<div class="seatBuyIns" hidden></div>
|
||||
|
||||
<details class="subPanel seatSubPanel cardsPanel">
|
||||
<summary><span class="seatSubPanelName">Hole cards</span> <span class="seatGlance cardsGlance"></span></summary>
|
||||
<div class="subPanelBody">
|
||||
<div class="seatCards">
|
||||
<button type="button" class="cardSlot card1Slot" aria-label="Choose first card"></button>
|
||||
<button type="button" class="cardSlot card2Slot" aria-label="Choose second card"></button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@* Named for the question it answers rather than for the boxes it contains. The
|
||||
counter, the typed override and the total are one calculation — what this
|
||||
seat has put in the pot — and the answer now sits on the summary, so a shut
|
||||
panel still says it. *@
|
||||
<details class="subPanel seatSubPanel contributionPanel">
|
||||
<summary><span class="seatSubPanelName">What they've put in</span> <span class="seatGlance contributionGlance"></span></summary>
|
||||
<div class="subPanelBody seatContribution">
|
||||
<div class="chipCounts"></div>
|
||||
<label class="fieldRow amountOverrideRow">
|
||||
<span>$ amount</span>
|
||||
<input type="number" class="amountOverrideInput" min="0" step="1" placeholder="auto" />
|
||||
</label>
|
||||
<div class="seatTotal">Total: <span class="seatTotalAmount">$0</span></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="subPanel seatSubPanel notesPanel">
|
||||
<summary><span class="seatSubPanelName">Notes on this seat</span> <span class="seatGlance notesGlance"></span></summary>
|
||||
<div class="subPanelBody">
|
||||
<div class="customFields"></div>
|
||||
<button class="holdemBtn holdemBtnSmall addFieldBtn">+ field</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="chipLegendRowTemplate">
|
||||
<div class="chipLegendRow">
|
||||
<input type="color" class="chipColorInput" />
|
||||
<input type="number" class="chipValueInput" min="0" step="0.01" placeholder="value" />
|
||||
<input type="color" class="chipColorInput" aria-label="Chip colour" />
|
||||
<input type="number" class="chipValueInput" min="0" step="0.01" placeholder="value" aria-label="Chip value" />
|
||||
<button class="holdemBtn holdemBtnSmall holdemBtnDanger removeChipColorBtn">×</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@* The value used to be a `title` attribute, which is a tooltip — and a tooltip is
|
||||
nothing at all on the device this is used on. Five coloured dots with five number
|
||||
boxes and no way to tell which one was the $25 without opening the Chip Legend and
|
||||
matching colours by eye. It is text now. *@
|
||||
<template id="chipCountTemplate">
|
||||
<label class="chipCountRow">
|
||||
<span class="chipSwatch"></span>
|
||||
<span class="chipCountValue"></span>
|
||||
<input type="number" class="chipCountInput" min="0" step="1" value="0" />
|
||||
</label>
|
||||
</template>
|
||||
@@ -196,6 +460,8 @@
|
||||
<span class="standingsIn"></span>
|
||||
<span class="standingsWon"></span>
|
||||
<span class="standingsNet"></span>
|
||||
<span class="standingsChips"></span>
|
||||
<span class="standingsCashOut"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -210,6 +476,240 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div id="addChipsOverlay" hidden>
|
||||
<div id="addChipsSheet" role="dialog" aria-labelledby="addChipsTitle">
|
||||
<div class="seatSheetGrip" aria-hidden="true"></div>
|
||||
<div class="addChipsHeader">
|
||||
<h2 id="addChipsTitle"></h2>
|
||||
<span id="addChipsSubtitle"></span>
|
||||
</div>
|
||||
<div id="addChipsQuick"></div>
|
||||
<div class="addChipsEntry">
|
||||
<span class="addChipsCurrency">$</span>
|
||||
<input type="text" id="addChipsInput" inputmode="decimal" autocomplete="off" aria-label="Amount to add" />
|
||||
</div>
|
||||
<div class="addChipsActions">
|
||||
<button id="addChipsConfirmBtn" class="holdemBtn holdemBtnAccent" type="button">Add to stack</button>
|
||||
<button id="addChipsCancelBtn" data-modal-close class="holdemBtn holdemBtnGhost" type="button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rulingsOverlay" hidden>
|
||||
<div id="rulingsSheet" role="dialog" aria-labelledby="rulingsTitle">
|
||||
<div class="seatSheetGrip" aria-hidden="true"></div>
|
||||
<div class="rulingsHeader">
|
||||
<h2 id="rulingsTitle">Common rulings</h2>
|
||||
<button id="closeRulingsBtn" data-modal-close class="holdemBtn holdemBtnSmall holdemBtnGhost" type="button">Done</button>
|
||||
</div>
|
||||
|
||||
<p class="rulingsIntro">
|
||||
The conventions most rooms follow, for the calls that come up mid-hand.
|
||||
Your house rules always win — and settle a dispute before the next hand,
|
||||
not during it.
|
||||
</p>
|
||||
|
||||
<nav id="rulingsIndex" class="rulingsIndex" aria-label="Jump to a ruling"></nav>
|
||||
|
||||
<section class="ruling" data-short="Misdeal">
|
||||
<h3>Redeal it — misdeal</h3>
|
||||
<ul>
|
||||
<li>The first card goes to the wrong seat.</li>
|
||||
<li>A seat entitled to a hand gets dealt out, or a seat that isn't entitled gets dealt in.</li>
|
||||
<li>Two or more boxed cards turn up on the initial deal.</li>
|
||||
<li>Either of the first two cards is exposed by dealer error.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="ruling" data-short="Board card early">
|
||||
<h3>A board card turned too early</h3>
|
||||
<p>
|
||||
If the fourth board card gets turned before the betting round is finished,
|
||||
that card is out of play for the round. Finish the betting, then burn and
|
||||
turn what would have been the fifth card in the fourth card's place.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="ruling" data-short="Out of turn">
|
||||
<h3>Acting out of turn</h3>
|
||||
<p>
|
||||
A one-off is usually just backed up if no one has acted behind them.
|
||||
Doing it repeatedly is what earns a penalty.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="ruling" data-short="Clock">
|
||||
<h3>Calling the clock on a player</h3>
|
||||
<p>
|
||||
Any player may ask for a clock once a decision is taking unreasonably
|
||||
long; it is the dealer, not the table, who grants it. The player then
|
||||
gets a set time and a countdown out loud — thirty seconds and ten is the
|
||||
usual form.
|
||||
</p>
|
||||
<p>
|
||||
If the count reaches zero with no action, the hand is <em>dead</em> if
|
||||
they were facing a bet, and <em>checked</em> if they were not: nobody is
|
||||
folded for slowness when checking was free. The action bar runs the
|
||||
count once you tap <em>Call the clock</em>, and offers the ruling as one
|
||||
button when it ends.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="ruling" data-short="Straddle">
|
||||
<h3>A straddle is a blind raise</h3>
|
||||
<p>
|
||||
The player left of the big blind may put out twice it before the cards,
|
||||
without looking. It is a blind bet, not a raise made in turn, so it does
|
||||
three things at once: everyone else acts first, the minimum raise becomes
|
||||
twice the straddle, and the straddler still has the option when the
|
||||
action comes back — they may raise even if nobody raised into them.
|
||||
</p>
|
||||
<p>
|
||||
Re-straddles carry on to the left, each doubling again, and stop before
|
||||
the small blind. Tap <em>Straddle</em> on the deal prompt and the tool
|
||||
posts it, puts the right player on the clock and prices the raise;
|
||||
the seat wears an <em>STR</em> badge for the hand.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="ruling" data-short="Short all-in">
|
||||
<h3>A short all-in doesn't reopen the betting</h3>
|
||||
<p>
|
||||
Somebody shoving in less than a full raise — say $3 more when the last
|
||||
raise was $8 — does not give the players behind them another go. Anyone
|
||||
who has already acted owes the difference or folds; they don't get to
|
||||
raise again because a short stack happened to land between them.
|
||||
</p>
|
||||
<p>
|
||||
A player who hadn't acted yet keeps full rights and may still raise, and
|
||||
the minimum raise stays measured off the last <em>full</em> raise, not
|
||||
off the short one. The action bar follows this for you.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="ruling" data-short="Uncalled bet">
|
||||
<h3>Chips nobody called come back</h3>
|
||||
<p>
|
||||
Bet more than anyone was able to call and the excess is returned to you
|
||||
before the showdown — it was never contested, so it isn't part of the
|
||||
pot. The pot breakdown shows it as "uncalled" rather than pretending it's
|
||||
a side pot you could win.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="ruling" data-short="Oversized chip">
|
||||
<h3>One big chip, nothing said</h3>
|
||||
<p>
|
||||
The most common argument at a home table. A player facing a bet who
|
||||
pushes out a single oversized chip without saying anything has
|
||||
<em>called</em> — not raised, however big the chip is. To raise with one
|
||||
chip you have to say "raise" before it lands; say it without an amount
|
||||
and the raise is the most that chip allows.
|
||||
</p>
|
||||
<p>
|
||||
Facing no bet, the same silent chip is a bet of the whole chip. And a
|
||||
clear verbal declaration in turn is binding either way — if words and
|
||||
chips disagree, whichever came first is the action.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="ruling" data-short="Minimum raise">
|
||||
<h3>A raise has to be a full raise</h3>
|
||||
<p>
|
||||
At least as much again as the last raise, or the big blind if there
|
||||
hasn't been one. Anything between a call and that minimum isn't a raise
|
||||
a player can make — it's a call, unless they're putting their last chips
|
||||
in. The raise pad says so if you type one, and won't stop you: your
|
||||
ruling beats the arithmetic.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="ruling" data-short="Who shows first">
|
||||
<h3>Who turns their hand over first</h3>
|
||||
<p>
|
||||
If there was a bet on the river, the last player to bet or raise shows
|
||||
first. If the river was checked all the way round, it's the first live
|
||||
player left of the button. Nobody has to be asked twice — a player may
|
||||
always turn their hand over early if they want to.
|
||||
</p>
|
||||
<p>
|
||||
When everyone is all-in before the river there was no final round to
|
||||
lead, so it falls to whoever last put money in, on whichever street that
|
||||
was. The action bar names the seat and the reason at every showdown, so
|
||||
the call is already made before anyone asks.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="ruling" data-short="Odd chip">
|
||||
<h3>The odd chip in a split pot</h3>
|
||||
<p>
|
||||
When a chopped pot won't divide evenly, the odd chip goes to the first
|
||||
player left of the button, not to whoever the dealer happened to tick
|
||||
first. Pot payouts here order the winners that way, so the leftover lands
|
||||
on the right seat every time.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="ruling" data-short="String bet">
|
||||
<h3>Betting in one motion — the string bet</h3>
|
||||
<p>
|
||||
A bet is whatever goes in on the first trip to the stack, unless the
|
||||
amount was said out loud first. Reaching back for more chips without
|
||||
having said "raise" is a string bet, and only the first motion stands.
|
||||
Saying the number is always enough on its own: "raise to sixty" binds
|
||||
whatever order the chips arrive in.
|
||||
</p>
|
||||
<p>
|
||||
It's the rule home games invoke most and enforce least, and both are
|
||||
fine — the point is that a player can't watch the table react and then
|
||||
decide how much to bet. Say it, or push it all at once.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p class="rulingsSource">
|
||||
Follows the Tournament Directors Association rules, which most rooms and
|
||||
home games take their lead from.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="seatMenuOverlay" hidden>
|
||||
<div id="seatMenuSheet" role="dialog" aria-labelledby="seatMenuTitle">
|
||||
<div class="seatSheetGrip" aria-hidden="true"></div>
|
||||
<div class="seatMenuHeader">
|
||||
<h2 id="seatMenuTitle"></h2>
|
||||
<span id="seatMenuSubtitle"></span>
|
||||
</div>
|
||||
<div id="seatMenuActions"></div>
|
||||
<button id="seatMenuCancelBtn" data-modal-close class="holdemBtn holdemBtnGhost" type="button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cardPickerOverlay" hidden>
|
||||
<div id="cardPickerSheet" role="dialog" aria-labelledby="cardPickerTitle">
|
||||
<div class="seatSheetGrip" aria-hidden="true"></div>
|
||||
<div class="cardPickerHeader">
|
||||
<h2 id="cardPickerTitle"></h2>
|
||||
<button id="cardPickerClearBtn" class="holdemBtn holdemBtnSmall" type="button">Clear</button>
|
||||
<button id="cardPickerCloseBtn" data-modal-close class="holdemBtn holdemBtnSmall holdemBtnGhost" type="button">Done</button>
|
||||
</div>
|
||||
<div id="cardPickerSlots"></div>
|
||||
<div id="cardPickerGrid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* No aria-live here any more. This element's entire contents are rebuilt on every
|
||||
action, so a live region on it announced the whole bar every time — measured, after
|
||||
one tap: "PRE-FLOP Pot $15 9 players Bob · $250 behind 0:00 ↶ dealing the hand U
|
||||
Fold Call $10 Raise". That is what a screen reader read out after every single fold.
|
||||
A live region has to be a stable element whose *text* changes; the one below is. *@
|
||||
<div id="actionBar" hidden></div>
|
||||
|
||||
@* Whose turn it is, in a sentence. Empty in the markup and only ever written to, so
|
||||
assistive tech is already watching it when the first update lands — a live region
|
||||
created at the moment it has something to say is one the screen reader misses. *@
|
||||
<div id="actionAnnouncer" role="status" aria-live="polite" class="visuallyHidden"></div>
|
||||
|
||||
<div id="toastHost" aria-live="polite"></div>
|
||||
|
||||
<div id="confirmOverlay" hidden>
|
||||
@@ -217,33 +717,79 @@
|
||||
<h2 id="confirmTitle"></h2>
|
||||
<p id="confirmMessage"></p>
|
||||
<div class="confirmActions">
|
||||
<button id="confirmCancelBtn" class="holdemBtn holdemBtnGhost" type="button">Cancel</button>
|
||||
<button id="confirmCancelBtn" data-modal-close class="holdemBtn holdemBtnGhost" type="button">Cancel</button>
|
||||
<button id="confirmAcceptBtn" class="holdemBtn holdemBtnDanger" type="button">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="potWizardOverlay" hidden>
|
||||
<div id="potWizardModal">
|
||||
<div id="potWizardModal" role="dialog" aria-labelledby="potWizardTitle">
|
||||
<div class="potWizardHeader">
|
||||
<h2>Resolve the Pot</h2>
|
||||
<button id="closePotWizardBtn" class="holdemBtn holdemBtnGhost">✕</button>
|
||||
<h2 id="potWizardTitle">Resolve the Pot</h2>
|
||||
<button id="closePotWizardBtn" data-modal-close class="holdemBtn holdemBtnGhost">✕</button>
|
||||
</div>
|
||||
<div id="potWizardBody"></div>
|
||||
@* The buttons live outside the scrolling body on purpose. This is the one
|
||||
dialog that divides money, and its Confirm sat below a scroll at every screen
|
||||
size — 244px off the bottom on an iPhone, 583 on a phone held sideways. A
|
||||
sticky footer cannot fix that from inside: the actions were the last child of
|
||||
the scrolling content, so their containing block ended exactly where they did
|
||||
and `bottom` had no room to lift them. Out here they simply don't scroll. *@
|
||||
<div id="potWizardFooter"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Styles {
|
||||
<link href="/css/casino/holdem.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
|
||||
|
||||
@* Installable, and works with no signal — see the service worker registration at the
|
||||
foot of main.js. A dealer running a night in somebody's back room is on the worst
|
||||
network there is, and the tool being a *website* was the last thing that could
|
||||
take it away from them mid-game. *@
|
||||
<link rel="manifest" href="/casino/holdem.webmanifest" />
|
||||
@* iOS reads none of the manifest's icons and only this. *@
|
||||
<link rel="apple-touch-icon" href="/images/casino/holdem-apple-touch.png" />
|
||||
@* The standard name first, then Apple's older one. Chrome has been printing
|
||||
"<meta name="apple-mobile-web-app-capable"> is deprecated" into this page's
|
||||
console on every load; the replacement is the unprefixed name, and Safari still
|
||||
wants the prefixed one, so both are here. *@
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Hold'em" />
|
||||
@* Paints the phone's chrome the same near-black as the felt's ground, so an
|
||||
installed copy doesn't sit under a white status bar. *@
|
||||
<meta name="theme-color" content="#141414" />
|
||||
}
|
||||
|
||||
@* A note for whoever reads the ?v= below and assumes it works: it doesn't. ViewData
|
||||
["cssVersion"] is assigned in _Layout's own code block, which runs after these
|
||||
sections are handed their values, so every one of these renders as a bare "?v=" —
|
||||
on this page and on Chess, Particles and Memory Lane too. It is inert rather than
|
||||
broken: Program.cs sends `Cache-Control: no-store` on every static file, so nothing
|
||||
is being browser-cached for a version string to bust in the first place.
|
||||
Left alone deliberately. Fixing it is a one-line change to a file every page on the
|
||||
site shares, and the service worker was the reason to care — that now uses stale-
|
||||
while-revalidate, which needs no version to stay current. *@
|
||||
@section Scripts {
|
||||
<script src="~/js/casino/holdem/money.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/feedback.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/potCalculator.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/state.js?v=@ViewData["cssVersion"]"></script>
|
||||
@* After feedback.js, because the first lock it takes announces itself with a Toast. *@
|
||||
<script src="~/js/casino/holdem/wakeLock.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/settleUp.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/render.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/handRank.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/cardPicker.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/seatMenu.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/addChips.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/betting.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/actionBar.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/shortcuts.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/potWizard.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/handHistoryExport.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/modal.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/main.js?v=@ViewData["cssVersion"]"></script>
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
<p class="splitSubtitle">Chop a pot without setting up a table.</p>
|
||||
</header>
|
||||
|
||||
<div id="modeToggle" class="chipRow" role="group" aria-label="How the money went in">
|
||||
<button type="button" id="modeEvenBtn" class="choiceChip selected">Everyone matched</button>
|
||||
<button type="button" id="modeUnevenBtn" class="choiceChip">Someone's all-in</button>
|
||||
</div>
|
||||
|
||||
<div id="splitResult" aria-live="polite">
|
||||
<div id="splitResultLabel">Each player gets</div>
|
||||
<div id="splitResultAmount">$0.00</div>
|
||||
@@ -18,7 +23,18 @@
|
||||
<div id="splitDustNote" hidden></div>
|
||||
</div>
|
||||
|
||||
<section class="splitField">
|
||||
<div id="unevenResult" hidden aria-live="polite">
|
||||
<div id="unevenPots"></div>
|
||||
<p id="unevenEmpty" class="unevenEmpty">Enter what each player put in.</p>
|
||||
</div>
|
||||
|
||||
<section class="splitField unevenOnly" hidden>
|
||||
<span class="splitLabel">What each player put in</span>
|
||||
<div id="unevenRows"></div>
|
||||
<button type="button" id="addPlayerBtn" class="splitBtn splitBtnGhost">+ Add player</button>
|
||||
</section>
|
||||
|
||||
<section class="splitField evenOnly">
|
||||
<label for="potInput" class="splitLabel">Pot</label>
|
||||
<div class="potInputWrap">
|
||||
<span class="potCurrency">$</span>
|
||||
@@ -28,12 +44,12 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="splitField">
|
||||
<section class="splitField evenOnly">
|
||||
<span class="splitLabel" id="waysLabel">Split between</span>
|
||||
<div id="waysChoices" class="chipRow" role="group" aria-labelledby="waysLabel"></div>
|
||||
</section>
|
||||
|
||||
<section class="splitField">
|
||||
<section class="splitField evenOnly">
|
||||
<span class="splitLabel" id="chipLabel">Smallest chip in play</span>
|
||||
<div id="chipChoices" class="chipRow" role="group" aria-labelledby="chipLabel"></div>
|
||||
</section>
|
||||
@@ -43,12 +59,30 @@
|
||||
<a href="/casino/texasholdem" class="splitBtn splitBtnGhost">Full dealer table →</a>
|
||||
</div>
|
||||
|
||||
<p class="splitFootnote">
|
||||
Uneven pots — someone all-in for less — need the side-pot walkthrough on the
|
||||
<a href="/casino/texasholdem">full table</a>.
|
||||
<p class="splitFootnote evenOnly">
|
||||
If somebody was all-in for less than the others, switch to
|
||||
<strong>Someone's all-in</strong> above.
|
||||
</p>
|
||||
|
||||
<p class="splitFootnote unevenOnly" hidden>
|
||||
Pots are listed with who can win each. Award them by hand strength, best hand
|
||||
first. Nothing here is saved — the <a href="/casino/texasholdem">full table</a>
|
||||
keeps a log.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template id="unevenRowTemplate">
|
||||
<div class="unevenRow">
|
||||
<span class="unevenRowLabel"></span>
|
||||
<div class="unevenAmountWrap">
|
||||
<span class="unevenCurrency">$</span>
|
||||
<input type="text" class="unevenAmount" inputmode="decimal" autocomplete="off" placeholder="0" />
|
||||
</div>
|
||||
<button type="button" class="unevenFoldBtn" aria-pressed="false">Folded</button>
|
||||
<button type="button" class="unevenRemoveBtn" aria-label="Remove player">✕</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@section Styles {
|
||||
<link href="/css/casino/quicksplit.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
|
||||
}
|
||||
@@ -56,5 +90,6 @@
|
||||
@section Scripts {
|
||||
<script src="~/js/casino/holdem/money.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/splitCalculator.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/potCalculator.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/quickSplit.js?v=@ViewData["cssVersion"]"></script>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
@page "/socialism/{slug}"
|
||||
@model JoshHeaps.Net.Pages.Socialism.DocModel
|
||||
@{
|
||||
ViewData["Title"] = Model.Doc!.Title + " - JoshHeaps.Net";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<div class="soc-progress" id="socProgress"></div>
|
||||
|
||||
<button class="soc-menu-toggle" id="socMenuToggle" aria-controls="socSidebar" aria-expanded="false">Contents</button>
|
||||
|
||||
<div class="soc-shell">
|
||||
<partial name="_Nav" model="Model" />
|
||||
|
||||
<div class="soc-main">
|
||||
<nav class="soc-breadcrumbs" aria-label="Breadcrumb">
|
||||
<a href="/socialism">Overview</a>
|
||||
@if (!string.IsNullOrEmpty(Model.Doc.Section))
|
||||
{
|
||||
<span>/</span><span>@Model.Doc.Section</span>
|
||||
}
|
||||
</nav>
|
||||
|
||||
<header class="soc-doc-header">
|
||||
<h1 class="soc-doc-title">@Model.Doc.Title</h1>
|
||||
@if (!string.IsNullOrEmpty(Model.Doc.Summary))
|
||||
{
|
||||
<p class="soc-doc-summary">@Model.Doc.Summary</p>
|
||||
}
|
||||
<span class="soc-doc-meta">@Model.Doc.ReadingMinutes min read</span>
|
||||
</header>
|
||||
|
||||
<article class="soc-content">
|
||||
@Html.Raw(Model.Doc.HtmlContent)
|
||||
</article>
|
||||
|
||||
<nav class="soc-pager" aria-label="Previous and next">
|
||||
@if (Model.Previous is not null)
|
||||
{
|
||||
<a class="soc-pager-link prev" href="@(Model.Previous.Slug == SocialismDocService.IndexSlug ? "/socialism" : "/socialism/" + Model.Previous.Slug)">
|
||||
<span class="soc-pager-label">← Previous</span>
|
||||
<span class="soc-pager-title">@(Model.Previous.Slug == SocialismDocService.IndexSlug ? "Overview" : Model.Previous.Title)</span>
|
||||
</a>
|
||||
}
|
||||
@if (Model.Next is not null)
|
||||
{
|
||||
<a class="soc-pager-link next" href="/socialism/@Model.Next.Slug">
|
||||
<span class="soc-pager-label">Next →</span>
|
||||
<span class="soc-pager-title">@Model.Next.Title</span>
|
||||
</a>
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<partial name="_Toc" model="Model.Doc.Headings" />
|
||||
</div>
|
||||
|
||||
@section Styles {
|
||||
<partial name="_Head" />
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Services.Implementations;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace JoshHeaps.Net.Pages.Socialism;
|
||||
|
||||
public class DocModel(ISocialismDocService docService) : SocialismPageModel
|
||||
{
|
||||
public SocialismDoc? Doc { get; private set; }
|
||||
public SocialismDoc? Previous { get; private set; }
|
||||
public SocialismDoc? Next { get; private set; }
|
||||
|
||||
public IActionResult OnGet(string slug)
|
||||
{
|
||||
if (slug == SocialismDocService.IndexSlug)
|
||||
return RedirectPermanent("/socialism");
|
||||
|
||||
Doc = docService.GetDoc(slug);
|
||||
if (Doc is null)
|
||||
return NotFound();
|
||||
|
||||
CurrentSlug = slug;
|
||||
AllDocs = docService.GetAllDocs();
|
||||
|
||||
var position = AllDocs.FindIndex(d => d.Slug == slug);
|
||||
Previous = position > 0 ? AllDocs[position - 1] : null;
|
||||
Next = position >= 0 && position < AllDocs.Count - 1 ? AllDocs[position + 1] : null;
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
@page "/socialism"
|
||||
@model JoshHeaps.Net.Pages.Socialism.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = Model.Overview!.Title + " - JoshHeaps.Net";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<div class="soc-progress" id="socProgress"></div>
|
||||
|
||||
<button class="soc-menu-toggle" id="socMenuToggle" aria-controls="socSidebar" aria-expanded="false">Contents</button>
|
||||
|
||||
<div class="soc-shell">
|
||||
<partial name="_Nav" model="Model" />
|
||||
|
||||
<div class="soc-main">
|
||||
<header class="soc-hero">
|
||||
<h1 class="soc-hero-title">@Model.Overview.Title</h1>
|
||||
@if (!string.IsNullOrEmpty(Model.Overview.Summary))
|
||||
{
|
||||
<p class="soc-hero-summary">@Model.Overview.Summary</p>
|
||||
}
|
||||
@if (Model.Chapters.Count > 0)
|
||||
{
|
||||
<a class="soc-hero-cta" href="/socialism/@Model.Chapters[0].Slug">Start reading →</a>
|
||||
}
|
||||
</header>
|
||||
|
||||
<article class="soc-content">
|
||||
@Html.Raw(Model.Overview.HtmlContent)
|
||||
</article>
|
||||
|
||||
@if (Model.Chapters.Count > 0)
|
||||
{
|
||||
<section class="soc-chapters" id="explore">
|
||||
<h2 class="soc-chapters-title">Explore the full model</h2>
|
||||
@foreach (var group in Model.Chapters.GroupBy(d => d.Section))
|
||||
{
|
||||
@if (!string.IsNullOrEmpty(group.Key))
|
||||
{
|
||||
<h3 class="soc-chapters-group">@group.Key</h3>
|
||||
}
|
||||
<div class="soc-card-grid">
|
||||
@foreach (var doc in group)
|
||||
{
|
||||
<a class="soc-card" href="/socialism/@doc.Slug">
|
||||
<span class="soc-card-title">@doc.Title</span>
|
||||
@if (!string.IsNullOrEmpty(doc.Summary))
|
||||
{
|
||||
<span class="soc-card-summary">@doc.Summary</span>
|
||||
}
|
||||
<span class="soc-card-meta">@doc.ReadingMinutes min read</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
|
||||
<partial name="_Toc" model="Model.Overview.Headings" />
|
||||
</div>
|
||||
|
||||
@section Styles {
|
||||
<partial name="_Head" />
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Services.Implementations;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace JoshHeaps.Net.Pages.Socialism;
|
||||
|
||||
public class IndexModel(ISocialismDocService docService) : SocialismPageModel
|
||||
{
|
||||
public SocialismDoc? Overview { get; private set; }
|
||||
public List<SocialismDoc> Chapters { get; private set; } = [];
|
||||
|
||||
public IActionResult OnGet()
|
||||
{
|
||||
Overview = docService.GetDoc(SocialismDocService.IndexSlug);
|
||||
if (Overview is null)
|
||||
return NotFound();
|
||||
|
||||
CurrentSlug = SocialismDocService.IndexSlug;
|
||||
AllDocs = docService.GetAllDocs();
|
||||
Chapters = AllDocs.Where(d => d.Slug != SocialismDocService.IndexSlug).ToList();
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using JoshHeaps.Net.Models;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace JoshHeaps.Net.Pages.Socialism;
|
||||
|
||||
/// <summary>
|
||||
/// Shared base for the /socialism pages. These pages are unlisted: nothing links to them
|
||||
/// and search engines are told not to index them.
|
||||
/// </summary>
|
||||
public abstract class SocialismPageModel : PageModel
|
||||
{
|
||||
public List<SocialismDoc> AllDocs { get; protected set; } = [];
|
||||
public string CurrentSlug { get; protected set; } = string.Empty;
|
||||
|
||||
public override void OnPageHandlerExecuting(PageHandlerExecutingContext context)
|
||||
{
|
||||
Response.Headers["X-Robots-Tag"] = "noindex, nofollow";
|
||||
base.OnPageHandlerExecuting(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link href="/css/variables.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
|
||||
<link href="/css/socialism/socialism.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
|
||||
<script src="/js/socialism/socialism.js?v=@ViewData["cssVersion"]" defer></script>
|
||||
@@ -0,0 +1,32 @@
|
||||
@model SocialismPageModel
|
||||
@{
|
||||
var groups = Model.AllDocs
|
||||
.Where(d => d.Slug != SocialismDocService.IndexSlug)
|
||||
.GroupBy(d => d.Section);
|
||||
}
|
||||
|
||||
<nav class="soc-sidebar" id="socSidebar" aria-label="Document navigation">
|
||||
<a href="/socialism" class="soc-sidebar-home @(Model.CurrentSlug == SocialismDocService.IndexSlug ? "active" : "")">Overview</a>
|
||||
|
||||
<input type="search" class="soc-filter" id="socFilter" placeholder="Filter sections..." aria-label="Filter sections" />
|
||||
|
||||
@foreach (var group in groups)
|
||||
{
|
||||
<div class="soc-nav-group">
|
||||
@if (!string.IsNullOrEmpty(group.Key))
|
||||
{
|
||||
<div class="soc-nav-group-title">@group.Key</div>
|
||||
}
|
||||
<ul>
|
||||
@foreach (var doc in group)
|
||||
{
|
||||
<li>
|
||||
<a href="/socialism/@doc.Slug"
|
||||
class="soc-nav-link @(doc.Slug == Model.CurrentSlug ? "active" : "")"
|
||||
data-filter-text="@doc.Title.ToLowerInvariant() @doc.Summary.ToLowerInvariant()">@doc.Title</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</nav>
|
||||
@@ -0,0 +1,16 @@
|
||||
@model List<SocialismHeading>
|
||||
|
||||
@if (Model.Count > 1)
|
||||
{
|
||||
<aside class="soc-toc" aria-label="On this page">
|
||||
<div class="soc-toc-title">On this page</div>
|
||||
<ul>
|
||||
@foreach (var heading in Model)
|
||||
{
|
||||
<li class="[email protected]">
|
||||
<a href="#@heading.Id" data-toc-id="@heading.Id">@heading.Text</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</aside>
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using JoshHeaps.Net.Models
|
||||
@using JoshHeaps.Net.Pages.Socialism
|
||||
@using JoshHeaps.Net.Services.Implementations
|
||||
@@ -1,6 +1,7 @@
|
||||
using JoshHeaps.Net.Hubs;
|
||||
using JoshHeaps.Net.Services.Implementations;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -19,6 +20,7 @@ builder.Services.AddHttpClient("BlogApi", client =>
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
});
|
||||
builder.Services.AddSingleton<IBlogService, BlogService>();
|
||||
builder.Services.AddSingleton<ISocialismDocService, SocialismDocService>();
|
||||
builder.Services.AddSingleton<IChessService, ChessService>();
|
||||
builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
||||
|
||||
@@ -52,8 +54,49 @@ if (!app.Environment.IsDevelopment())
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
// One spelling of the casino URLs, because the Hold'em tool's offline support depends
|
||||
// on it and nothing warns you when it breaks.
|
||||
//
|
||||
// Razor Pages routing is case-insensitive, so /Casino/TexasHoldem and
|
||||
// /casino/texasholdem both serve the dealer tool and look identical. A service worker's
|
||||
// scope is not: the spec matches it as a plain string prefix, so a worker scoped to
|
||||
// "/casino/" does not control a page at "/Casino/TexasHoldem". Measured, that meant the
|
||||
// capital-C spelling registered the worker, activated it, logged nothing — and then left
|
||||
// the cache completely empty, because the page was never controlled and the precache
|
||||
// handshake it waited on could not resolve. A dealer who bookmarked that URL had a tool
|
||||
// that looked loaded and had nothing stored for the night the signal went.
|
||||
//
|
||||
// So one spelling wins and the other is redirected to it, permanently, before anything
|
||||
// else in the pipeline gets a look.
|
||||
//
|
||||
// Deliberately scoped to /casino rather than applied site-wide. Every file under
|
||||
// wwwroot/casino and wwwroot/images/casino is already lowercase, so lowercasing a path
|
||||
// here can never miss a real file on a case-sensitive filesystem — a guarantee that does
|
||||
// not hold for the rest of the site, and one that /chessHub in particular would break.
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var path = context.Request.Path.Value;
|
||||
if (path is not null
|
||||
&& path.StartsWith("/casino", StringComparison.OrdinalIgnoreCase)
|
||||
&& path.Any(char.IsUpper))
|
||||
{
|
||||
context.Response.Redirect(path.ToLowerInvariant() + context.Request.QueryString, permanent: true);
|
||||
return;
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
// .webmanifest isn't in the default MIME map, and an unknown extension isn't served
|
||||
// with the wrong type — it isn't served at all. Without this the Hold'em tool's
|
||||
// manifest 404s, and a 404 manifest is exactly the silent failure that stops a browser
|
||||
// ever offering "Add to Home Screen": nothing is logged and no prompt appears.
|
||||
var contentTypes = new FileExtensionContentTypeProvider();
|
||||
contentTypes.Mappings[".webmanifest"] = "application/manifest+json";
|
||||
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
ContentTypeProvider = contentTypes,
|
||||
OnPrepareResponse = ctx =>
|
||||
{
|
||||
ctx.Context.Response.Headers.Append("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
using Markdig;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace JoshHeaps.Net.Services.Implementations;
|
||||
|
||||
/// <summary>
|
||||
/// Serves the markdown documents in Content/Socialism. Files starting with an underscore
|
||||
/// (the founding draft, the progress log) are private and never served.
|
||||
/// </summary>
|
||||
public partial class SocialismDocService : ISocialismDocService
|
||||
{
|
||||
public const string IndexSlug = "index";
|
||||
|
||||
private readonly string _contentDir;
|
||||
private readonly ILogger<SocialismDocService> _logger;
|
||||
private readonly ConcurrentDictionary<string, CacheEntry> _cache = new();
|
||||
|
||||
private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder()
|
||||
.UseAdvancedExtensions()
|
||||
.Build();
|
||||
|
||||
public SocialismDocService(IWebHostEnvironment env, ILogger<SocialismDocService> logger)
|
||||
{
|
||||
_contentDir = Path.Combine(env.ContentRootPath, "Content", "Socialism");
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public SocialismDoc? GetDoc(string slug)
|
||||
{
|
||||
if (!IsValidSlug(slug))
|
||||
return null;
|
||||
|
||||
var path = Path.Combine(_contentDir, slug + ".md");
|
||||
if (!File.Exists(path))
|
||||
return null;
|
||||
|
||||
// Cache keyed on last-write time so edited documents show up without a restart.
|
||||
var lastWrite = File.GetLastWriteTimeUtc(path);
|
||||
if (_cache.TryGetValue(slug, out var entry) && entry.LastWrite == lastWrite)
|
||||
return entry.Doc;
|
||||
|
||||
try
|
||||
{
|
||||
var doc = Parse(slug, File.ReadAllText(path));
|
||||
_cache[slug] = new CacheEntry(doc, lastWrite);
|
||||
return doc;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to render socialism doc {Slug}", slug);
|
||||
return entry?.Doc;
|
||||
}
|
||||
}
|
||||
|
||||
public List<SocialismDoc> GetAllDocs()
|
||||
{
|
||||
if (!Directory.Exists(_contentDir))
|
||||
return [];
|
||||
|
||||
return Directory.EnumerateFiles(_contentDir, "*.md")
|
||||
.Select(Path.GetFileNameWithoutExtension)
|
||||
.Where(slug => slug is not null && IsValidSlug(slug))
|
||||
.Select(slug => GetDoc(slug!))
|
||||
.OfType<SocialismDoc>()
|
||||
.OrderBy(d => d.Slug == IndexSlug ? 0 : 1)
|
||||
.ThenBy(d => d.Order)
|
||||
.ThenBy(d => d.Title)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Lowercase letters, digits and hyphens only: rules out private "_" files and path traversal.
|
||||
private static bool IsValidSlug(string slug) => SlugRegex().IsMatch(slug);
|
||||
|
||||
private static SocialismDoc Parse(string slug, string raw)
|
||||
{
|
||||
var (meta, body) = SplitFrontMatter(raw);
|
||||
var markdown = Markdown.Parse(body, Pipeline);
|
||||
|
||||
var headings = new List<SocialismHeading>();
|
||||
string? firstH1 = null;
|
||||
foreach (var heading in markdown.Descendants<HeadingBlock>())
|
||||
{
|
||||
var text = InlineText(heading.Inline);
|
||||
if (heading.Level == 1)
|
||||
{
|
||||
firstH1 ??= text;
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = heading.GetAttributes().Id;
|
||||
if (heading.Level <= 3 && !string.IsNullOrEmpty(id))
|
||||
headings.Add(new SocialismHeading(heading.Level, id, text));
|
||||
}
|
||||
|
||||
foreach (var link in markdown.Descendants<LinkInline>())
|
||||
{
|
||||
if (!link.IsImage && link.Url is not null)
|
||||
link.Url = RewriteLink(link.Url);
|
||||
}
|
||||
|
||||
var words = WordRegex().Matches(body).Count;
|
||||
|
||||
return new SocialismDoc
|
||||
{
|
||||
Slug = slug,
|
||||
Title = meta.GetValueOrDefault("title") ?? firstH1 ?? slug,
|
||||
Summary = meta.GetValueOrDefault("summary") ?? string.Empty,
|
||||
Section = meta.GetValueOrDefault("section") ?? string.Empty,
|
||||
Order = int.TryParse(meta.GetValueOrDefault("order"), out var order) ? order : int.MaxValue,
|
||||
HtmlContent = markdown.ToHtml(Pipeline),
|
||||
Headings = headings,
|
||||
ReadingMinutes = Math.Max(1, (int)Math.Round(words / 230.0))
|
||||
};
|
||||
}
|
||||
|
||||
// "economy.md#floor" -> "/socialism/economy#floor"; "index.md" -> "/socialism".
|
||||
private static string RewriteLink(string url)
|
||||
{
|
||||
var match = RelativeDocLinkRegex().Match(url);
|
||||
if (!match.Success)
|
||||
return url;
|
||||
|
||||
var target = match.Groups["slug"].Value;
|
||||
var anchor = match.Groups["anchor"].Value;
|
||||
return (target == IndexSlug ? "/socialism" : $"/socialism/{target}") + anchor;
|
||||
}
|
||||
|
||||
private static (Dictionary<string, string> Meta, string Body) SplitFrontMatter(string raw)
|
||||
{
|
||||
var meta = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var match = FrontMatterRegex().Match(raw);
|
||||
if (!match.Success)
|
||||
return (meta, raw);
|
||||
|
||||
foreach (var line in match.Groups["yaml"].Value.Split('\n'))
|
||||
{
|
||||
var idx = line.IndexOf(':');
|
||||
if (idx <= 0)
|
||||
continue;
|
||||
meta[line[..idx].Trim()] = line[(idx + 1)..].Trim().Trim('"');
|
||||
}
|
||||
|
||||
return (meta, raw[match.Length..]);
|
||||
}
|
||||
|
||||
private static string InlineText(ContainerInline? inline)
|
||||
{
|
||||
if (inline is null)
|
||||
return string.Empty;
|
||||
|
||||
return string.Concat(inline.Descendants<Inline>().Select(i => i switch
|
||||
{
|
||||
LiteralInline literal => literal.Content.ToString(),
|
||||
CodeInline code => code.Content,
|
||||
_ => string.Empty
|
||||
}));
|
||||
}
|
||||
|
||||
[GeneratedRegex("^[a-z0-9][a-z0-9-]*$")]
|
||||
private static partial Regex SlugRegex();
|
||||
|
||||
[GeneratedRegex(@"^(?:\./)?(?<slug>[a-z0-9][a-z0-9-]*)\.md(?<anchor>#[^\s]*)?$")]
|
||||
private static partial Regex RelativeDocLinkRegex();
|
||||
|
||||
[GeneratedRegex(@"\A---\r?\n(?<yaml>.*?)\r?\n---\r?\n", RegexOptions.Singleline)]
|
||||
private static partial Regex FrontMatterRegex();
|
||||
|
||||
[GeneratedRegex(@"\b\w+\b")]
|
||||
private static partial Regex WordRegex();
|
||||
|
||||
private record CacheEntry(SocialismDoc Doc, DateTime LastWrite);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using JoshHeaps.Net.Models;
|
||||
|
||||
namespace JoshHeaps.Net.Services.Interfaces;
|
||||
|
||||
public interface ISocialismDocService
|
||||
{
|
||||
SocialismDoc? GetDoc(string slug);
|
||||
List<SocialismDoc> GetAllDocs();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Texas Hold'em Dealer",
|
||||
"short_name": "Hold'em",
|
||||
"description": "Seat your players, track the action, split the pot. Works with no signal.",
|
||||
"start_url": "/casino/texasholdem",
|
||||
"scope": "/casino/",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"background_color": "#141414",
|
||||
"theme_color": "#141414",
|
||||
"categories": ["utilities"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/casino/holdem-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/images/casino/holdem-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/images/casino/holdem-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Keeps the dealer tool working when the network doesn't.
|
||||
//
|
||||
// A home game is the worst network in the world: somebody's back room, one bar of
|
||||
// signal, a router two floors away. The tool already keeps the whole night in
|
||||
// localStorage, so the *table* survives anything — but the page didn't. Reload with no
|
||||
// signal, or open it fresh when the wifi has dropped, and a dealer holding a phone in
|
||||
// one hand and a deck in the other got a browser error page with nine players waiting.
|
||||
//
|
||||
// This is the one thing standing between "a website about poker" and "a tool you can
|
||||
// rely on at a table", so it is worth the moving part.
|
||||
//
|
||||
// Scope is /casino/ rather than /casino/texasholdem/, because a scope has to be a
|
||||
// prefix of the page it controls and "/casino/texasholdem" does not start with
|
||||
// "/casino/texasholdem/". Anything in that scope this worker has not been told about
|
||||
// goes straight to the network, so the other casino pages behave exactly as they did.
|
||||
|
||||
// Bumped whenever the shape of what is cached changes. The asset *version* rides in
|
||||
// the URLs themselves (?v=1.0.8 from _Layout), so a normal release makes new keys
|
||||
// without touching this.
|
||||
const CACHE = "holdem-shell-v1";
|
||||
|
||||
// What this worker will keep. Anything else in scope — a form post, an API call, a
|
||||
// page it was never told about — goes straight to the network and behaves exactly as
|
||||
// it did before the worker existed.
|
||||
const CACHEABLE = new Set(["script", "style", "image", "font", "manifest"]);
|
||||
|
||||
// The page hands over the exact list of URLs it was built from — see registerWorker()
|
||||
// in main.js. Deriving it from the document instead of keeping a list here is the
|
||||
// whole trick: a hand-written manifest of files drifts from the markup the first time
|
||||
// somebody adds a script tag, and the failure mode is a tool that is offline-capable
|
||||
// right up until the release that quietly broke it.
|
||||
async function precache(urls) {
|
||||
const cache = await caches.open(CACHE);
|
||||
// Individually rather than cache.addAll, which rejects the whole batch if any one
|
||||
// URL 404s — one stale entry in the list would leave the dealer with nothing.
|
||||
await Promise.all(urls.map(async url => {
|
||||
try {
|
||||
const res = await fetch(url, { cache: "reload" });
|
||||
if (res.ok) await cache.put(url, res);
|
||||
} catch {
|
||||
// Offline while installing. The runtime handler below picks these up on
|
||||
// the next online visit.
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// No install handler, and deliberately no skipWaiting(). A new worker taking over a
|
||||
// live page can hand it assets from a different release than the ones it started with,
|
||||
// and this page loads seventeen separate scripts that expect to agree with each other.
|
||||
// Waiting costs a dealer nothing — the update lands the next time they open the tool —
|
||||
// and it cannot leave them with a half-swapped app in the middle of a hand.
|
||||
|
||||
self.addEventListener("activate", event => {
|
||||
event.waitUntil((async () => {
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(keys
|
||||
.filter(key => key.startsWith("holdem-shell-") && key !== CACHE)
|
||||
.map(key => caches.delete(key)));
|
||||
await self.clients.claim();
|
||||
})());
|
||||
});
|
||||
|
||||
self.addEventListener("message", event => {
|
||||
if (event.data && event.data.type === "precache" && Array.isArray(event.data.urls)) {
|
||||
event.waitUntil(precache(event.data.urls));
|
||||
}
|
||||
});
|
||||
|
||||
// Only GETs, and only same-origin. A cross-origin request has opaque responses that
|
||||
// can't be inspected, and caching one would mean serving a failure forever.
|
||||
function handles(request) {
|
||||
if (request.method !== "GET") return false;
|
||||
const url = new URL(request.url);
|
||||
return url.origin === self.location.origin;
|
||||
}
|
||||
|
||||
self.addEventListener("fetch", event => {
|
||||
const { request } = event;
|
||||
if (!handles(request)) return;
|
||||
|
||||
// The page itself: network first, so an online dealer always gets the current
|
||||
// build, and the cached copy is there the moment they aren't.
|
||||
if (request.mode === "navigate") {
|
||||
event.respondWith((async () => {
|
||||
try {
|
||||
const fresh = await fetch(request);
|
||||
if (fresh.ok) (await caches.open(CACHE)).put(request, fresh.clone());
|
||||
return fresh;
|
||||
} catch {
|
||||
const cached = await caches.match(request, { ignoreSearch: true });
|
||||
if (cached) return cached;
|
||||
throw new Error("offline and this page was never cached");
|
||||
}
|
||||
})());
|
||||
return;
|
||||
}
|
||||
|
||||
// Assets: stale while revalidate. The cached copy answers immediately — which is
|
||||
// the whole point, a dealer opening this between hands should not wait for a
|
||||
// network — and a fresh copy is fetched in the background for next time.
|
||||
//
|
||||
// Cache-first would be the obvious choice, on the reasoning that these URLs carry
|
||||
// ?v= from the layout and so a hit is always the right file. They don't: the
|
||||
// version renders empty (see the note in TexasHoldem.cshtml), so every release
|
||||
// reuses the same URLs. Cache-first against unversioned URLs is how a tool gets
|
||||
// frozen on a build from three months ago with no way for anyone to tell. This
|
||||
// strategy needs no version to be correct: worst case a dealer is one launch
|
||||
// behind, which for a tool that runs entirely in the browser is nothing.
|
||||
if (!CACHEABLE.has(request.destination)) return;
|
||||
|
||||
event.respondWith((async () => {
|
||||
const cache = await caches.open(CACHE);
|
||||
const cached = await cache.match(request);
|
||||
|
||||
const fresh = fetch(request).then(res => {
|
||||
if (res.ok) cache.put(request, res.clone());
|
||||
return res;
|
||||
}).catch(() => null);
|
||||
|
||||
if (cached) return cached;
|
||||
const res = await fresh;
|
||||
if (res) return res;
|
||||
throw new Error("offline and not cached: " + request.url);
|
||||
})());
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,58 @@ body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* Same touch handling as the dealer table — see the note there. This page is the one
|
||||
a dealer opens mid-hand to chop a pot, so the ~300ms the browser spends deciding
|
||||
whether a tap was a double-tap-to-zoom is the last thing it needs. */
|
||||
button,
|
||||
[role="button"],
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
a {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
button,
|
||||
[role="button"],
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
a {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Not on the fields — a dealer typing a pot amount still needs to select it. */
|
||||
button,
|
||||
[role="button"] {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.clearPotBtn:active:not(:disabled),
|
||||
.choiceChip:active:not(:disabled),
|
||||
.stepBtn:active:not(:disabled),
|
||||
.splitBtn:active:not(:disabled),
|
||||
.splitBtnGhost:active:not(:disabled),
|
||||
.unevenFoldBtn:active:not(:disabled),
|
||||
.unevenRemoveBtn:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
filter: brightness(1.25);
|
||||
transition-duration: 0.04s;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.clearPotBtn:active:not(:disabled),
|
||||
.choiceChip:active:not(:disabled),
|
||||
.stepBtn:active:not(:disabled),
|
||||
.splitBtn:active:not(:disabled),
|
||||
.splitBtnGhost:active:not(:disabled),
|
||||
.unevenFoldBtn:active:not(:disabled),
|
||||
.unevenRemoveBtn:active:not(:disabled) {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
#splitApp {
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
@@ -45,6 +97,256 @@ body {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ---------- Mode toggle ---------- */
|
||||
|
||||
#modeToggle {
|
||||
margin-bottom: 1.1rem;
|
||||
}
|
||||
|
||||
/* ---------- Uneven amounts ---------- */
|
||||
|
||||
#unevenRows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.unevenRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.unevenRowLabel {
|
||||
flex: none;
|
||||
width: 2rem;
|
||||
color: #8f8f8f;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.unevenAmountWrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
background: #141414;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 10px;
|
||||
padding: 0 0.6rem;
|
||||
}
|
||||
|
||||
.unevenAmountWrap:focus-within {
|
||||
border-color: #d8b46a;
|
||||
}
|
||||
|
||||
.unevenCurrency {
|
||||
/* Was #777 — 4.11:1 on this background, under the 4.5:1 small text needs. It sits
|
||||
against every amount a dealer types, so it is read constantly. */
|
||||
color: #8f8f8f;
|
||||
}
|
||||
|
||||
/* 37px tall, on the field a dealer types every amount into. The touch rule has been
|
||||
44px everywhere else since the round that set it; this page never got measured for
|
||||
it, because the audit only ran the target check when asked and nothing asked. */
|
||||
.unevenAmount {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 46px;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #f2f2f2;
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
padding: 0.6rem 0;
|
||||
}
|
||||
|
||||
.unevenFoldBtn {
|
||||
flex: none;
|
||||
min-height: 44px;
|
||||
padding: 0 0.6rem;
|
||||
border-radius: 9px;
|
||||
border: 1px solid #3a3a3a;
|
||||
background: #232323;
|
||||
color: #8f8f8f;
|
||||
font-family: inherit;
|
||||
font-size: 0.72rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.unevenFoldBtn.selected {
|
||||
background: linear-gradient(180deg, #3d1f1f, #2c1515);
|
||||
border-color: #5b2626;
|
||||
color: #ffd6d6;
|
||||
}
|
||||
|
||||
/* 30px across, and it deletes a player's entry — the one control on the row where a
|
||||
near miss costs something. The amount field beside it gives up the 16px. */
|
||||
.unevenRemoveBtn {
|
||||
flex: none;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border: none;
|
||||
background: none;
|
||||
/* Was #6a6a6a — 3.66:1, the worst on the page, on a control that deletes a
|
||||
player's entry. A destructive action should not be the faintest thing in view. */
|
||||
color: #9a9a9a;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.unevenRemoveBtn:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#unevenResult {
|
||||
background: linear-gradient(160deg, #1f2a21, #161c17);
|
||||
border: 1px solid #2c3a2f;
|
||||
border-radius: 14px;
|
||||
padding: 0.9rem 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
#unevenResult[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.unevenPot {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
/* Wraps so a chop opens underneath its own pot rather than squeezing in beside the
|
||||
eligible-players list and pushing it off the row. */
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
padding: 0.3rem 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.unevenChopToggle {
|
||||
flex: none;
|
||||
order: 3;
|
||||
/* 44px on touch: it moves money, like everything else that does. */
|
||||
min-height: 32px;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border: 1px solid rgba(216, 180, 106, 0.45);
|
||||
border-radius: 999px;
|
||||
background: rgba(216, 180, 106, 0.1);
|
||||
color: #e3c887;
|
||||
font: inherit;
|
||||
font-size: 0.74rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.unevenChopToggle:hover {
|
||||
background: rgba(216, 180, 106, 0.18);
|
||||
}
|
||||
|
||||
.unevenChopToggle:focus-visible {
|
||||
outline: 2px solid #d8b46a;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.unevenChopDetail {
|
||||
order: 4;
|
||||
flex: 0 0 100%;
|
||||
/* Or the padding is added on top of the 100% basis and the panel sits wider than
|
||||
the row it belongs to — measured 328px inside a 309px row on a phone. */
|
||||
box-sizing: border-box;
|
||||
margin: 0.15rem 0 0.2rem;
|
||||
padding: 0.45rem 0.6rem;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.unevenChopWays {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.unevenChopWaysLabel {
|
||||
color: #cfcfcf;
|
||||
font-size: 0.78rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.unevenChopEach {
|
||||
margin: 0;
|
||||
color: #f2f2f2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.unevenChopNote {
|
||||
margin: 0.2rem 0 0;
|
||||
color: #9a9a9a;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.unevenChopDust {
|
||||
color: #e0b169;
|
||||
}
|
||||
|
||||
@media (max-width: 700px), (pointer: coarse) {
|
||||
.unevenChopToggle {
|
||||
min-height: 44px;
|
||||
padding: 0.4rem 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.unevenPot + .unevenPot {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.unevenPotLabel {
|
||||
flex: none;
|
||||
color: #d8b46a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.unevenPotAmount {
|
||||
flex: none;
|
||||
color: #f2f2f2;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.unevenPotWho {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: right;
|
||||
color: #9a9a9a;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.unevenPotReturn .unevenPotLabel,
|
||||
.unevenPotReturn .unevenPotAmount {
|
||||
color: #9a9a9a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.unevenPotTotal {
|
||||
margin-top: 0.15rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.14) !important;
|
||||
}
|
||||
|
||||
.unevenPotTotal .unevenPotLabel {
|
||||
color: #8f9d92;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.unevenEmpty {
|
||||
margin: 0;
|
||||
color: #8f9d92;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* ---------- The answer ---------- */
|
||||
|
||||
#splitResult {
|
||||
@@ -104,6 +406,12 @@ body {
|
||||
margin-bottom: 1.35rem;
|
||||
}
|
||||
|
||||
/* A `display` declaration outranks the browser's own [hidden] rule, so anything
|
||||
given one has to opt back out explicitly or toggling `hidden` does nothing. */
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.splitLabel {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
@@ -165,9 +473,11 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clearPotBtn:hover {
|
||||
background: #333;
|
||||
color: #ddd;
|
||||
@media (hover: hover) {
|
||||
.clearPotBtn:hover {
|
||||
background: #333;
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Tap targets ---------- */
|
||||
@@ -193,9 +503,11 @@ body {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.choiceChip:hover {
|
||||
border-color: #4a4a4a;
|
||||
background: linear-gradient(180deg, #383838, #2a2a2a);
|
||||
@media (hover: hover) {
|
||||
.choiceChip:hover {
|
||||
border-color: #4a4a4a;
|
||||
background: linear-gradient(180deg, #383838, #2a2a2a);
|
||||
}
|
||||
}
|
||||
|
||||
.choiceChip.selected {
|
||||
@@ -230,8 +542,10 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stepBtn:hover {
|
||||
background: #333;
|
||||
@media (hover: hover) {
|
||||
.stepBtn:hover {
|
||||
background: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.waysReadout {
|
||||
@@ -268,9 +582,11 @@ body {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.splitBtn:hover {
|
||||
border-color: #4a4a4a;
|
||||
background: linear-gradient(180deg, #383838, #2a2a2a);
|
||||
@media (hover: hover) {
|
||||
.splitBtn:hover {
|
||||
border-color: #4a4a4a;
|
||||
background: linear-gradient(180deg, #383838, #2a2a2a);
|
||||
}
|
||||
}
|
||||
|
||||
.splitBtnGhost {
|
||||
@@ -279,12 +595,23 @@ body {
|
||||
|
||||
.splitFootnote {
|
||||
margin-top: 1.5rem;
|
||||
color: #6f6f6f;
|
||||
/* Was #6f6f6f — 3.94:1. It carries the instruction for what to do with the pot
|
||||
list, which is not a decorative line. */
|
||||
color: #8d8d8d;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* This footnote names a control the dealer is being told to reach for, so the name
|
||||
itself has to be readable — it was 3.39:1 at its worst, inheriting the muted grey
|
||||
the surrounding sentence is deliberately set in. Emphasised text carrying the
|
||||
instruction gets its own colour rather than the aside's. */
|
||||
.splitFootnote strong {
|
||||
color: #a8a8a8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.splitFootnote a {
|
||||
color: #a9782f;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,557 @@
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-body);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── Reading progress ── */
|
||||
|
||||
.soc-progress {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
width: 0;
|
||||
background: var(--color-accent);
|
||||
z-index: 20;
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
/* ── Three-column shell: nav | content | on-this-page ── */
|
||||
|
||||
.soc-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 250px minmax(0, 760px) 220px;
|
||||
justify-content: center;
|
||||
gap: 3rem;
|
||||
padding: 0 clamp(1rem, 3vw, 2rem);
|
||||
}
|
||||
|
||||
.soc-main {
|
||||
padding: 4rem 0 6rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ── Sidebar ── */
|
||||
|
||||
.soc-sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
align-self: start;
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
padding: 4rem 0 2rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-border) transparent;
|
||||
}
|
||||
|
||||
.soc-sidebar-home {
|
||||
display: block;
|
||||
color: var(--color-heading-secondary);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.soc-sidebar-home.active,
|
||||
.soc-sidebar-home:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.soc-filter {
|
||||
width: 100%;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
color: var(--color-heading-secondary);
|
||||
padding: 0.45rem 0.65rem;
|
||||
font: inherit;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.soc-filter:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent-border);
|
||||
}
|
||||
|
||||
.soc-nav-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.soc-nav-group-title {
|
||||
color: var(--color-text-subtle);
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.soc-sidebar ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.soc-nav-link {
|
||||
display: block;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.3rem 0 0.3rem 0.75rem;
|
||||
border-left: 1px solid var(--color-border);
|
||||
transition: color 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.soc-nav-link:hover {
|
||||
color: var(--color-heading-secondary);
|
||||
border-left-color: var(--color-border-hover);
|
||||
}
|
||||
|
||||
.soc-nav-link.active {
|
||||
color: var(--color-accent);
|
||||
border-left-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.soc-nav-link.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── On this page ── */
|
||||
|
||||
.soc-toc {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
align-self: start;
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
padding: 4rem 0 2rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.soc-toc-title {
|
||||
color: var(--color-text-subtle);
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.soc-toc ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.soc-toc a {
|
||||
display: block;
|
||||
color: var(--color-text-subtle);
|
||||
text-decoration: none;
|
||||
padding: 0.2rem 0;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.soc-toc .soc-toc-level-3 a {
|
||||
padding-left: 0.85rem;
|
||||
}
|
||||
|
||||
.soc-toc a:hover {
|
||||
color: var(--color-heading-secondary);
|
||||
}
|
||||
|
||||
.soc-toc a.active {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* ── Landing hero ── */
|
||||
|
||||
.soc-hero {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.soc-hero-title {
|
||||
color: var(--color-heading);
|
||||
font-size: clamp(2rem, 4vw, 2.75rem);
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.soc-hero-summary {
|
||||
color: var(--color-heading-secondary);
|
||||
font-size: 1.15rem;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.soc-hero-cta {
|
||||
display: inline-block;
|
||||
color: var(--color-bg);
|
||||
background: var(--color-accent);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
padding: 0.55rem 1.1rem;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.soc-hero-cta:hover {
|
||||
background: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
/* ── Chapter cards ── */
|
||||
|
||||
.soc-chapters {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
.soc-chapters-title {
|
||||
color: var(--color-heading);
|
||||
font-size: 1.4rem;
|
||||
margin: 0 0 1.25rem;
|
||||
}
|
||||
|
||||
.soc-chapters-group {
|
||||
color: var(--color-text-subtle);
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin: 2rem 0 0.75rem;
|
||||
}
|
||||
|
||||
.soc-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.soc-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
padding: 1.1rem 1.2rem;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s, border-color 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.soc-card:hover {
|
||||
background: var(--color-surface-hover);
|
||||
border-color: var(--color-accent-border);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.soc-card-title {
|
||||
color: var(--color-heading-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.soc-card-summary {
|
||||
color: var(--color-text);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.soc-card-meta {
|
||||
color: var(--color-text-subtle);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* ── Document header ── */
|
||||
|
||||
.soc-breadcrumbs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-subtle);
|
||||
}
|
||||
|
||||
.soc-breadcrumbs a {
|
||||
color: var(--color-text-subtle);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.soc-breadcrumbs a:hover {
|
||||
color: var(--color-heading-secondary);
|
||||
}
|
||||
|
||||
.soc-doc-header {
|
||||
margin: 1.5rem 0 2.5rem;
|
||||
}
|
||||
|
||||
.soc-doc-title {
|
||||
color: var(--color-heading);
|
||||
font-size: clamp(1.75rem, 3vw, 2.25rem);
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.soc-doc-summary {
|
||||
color: var(--color-heading-secondary);
|
||||
font-size: 1.05rem;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.soc-doc-meta {
|
||||
color: var(--color-text-subtle);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* ── Rendered markdown ── */
|
||||
|
||||
.soc-content {
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.soc-content > h1:first-child {
|
||||
display: none; /* the page header already shows the title */
|
||||
}
|
||||
|
||||
.soc-content h2,
|
||||
.soc-content h3,
|
||||
.soc-content h4 {
|
||||
position: relative;
|
||||
color: var(--color-heading-secondary);
|
||||
letter-spacing: -0.01em;
|
||||
margin: 2.5rem 0 0.75rem;
|
||||
}
|
||||
|
||||
.soc-content h2 {
|
||||
font-size: 1.45rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.soc-content h3 { font-size: 1.15rem; }
|
||||
|
||||
.soc-anchor {
|
||||
position: absolute;
|
||||
left: -1.2em;
|
||||
color: var(--color-text-subtle);
|
||||
text-decoration: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.soc-content h2:hover .soc-anchor,
|
||||
.soc-content h3:hover .soc-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.soc-content p {
|
||||
margin: 0 0 1.25rem;
|
||||
}
|
||||
|
||||
.soc-content a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.soc-content a:hover {
|
||||
color: var(--color-accent-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.soc-content strong {
|
||||
color: var(--color-heading-secondary);
|
||||
}
|
||||
|
||||
.soc-content ul,
|
||||
.soc-content ol {
|
||||
margin: 0 0 1.25rem 1.5rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.soc-content li {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.soc-content blockquote {
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-left: 3px solid var(--color-accent);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-heading-secondary);
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.soc-content blockquote p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.soc-content hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin: 2.5rem 0;
|
||||
}
|
||||
|
||||
.soc-content code {
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-heading-secondary);
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.soc-content table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0 0 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.soc-content th,
|
||||
.soc-content td {
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.soc-content th {
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-heading-secondary);
|
||||
}
|
||||
|
||||
.soc-content details {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0 0 1.25rem;
|
||||
}
|
||||
|
||||
.soc-content summary {
|
||||
cursor: pointer;
|
||||
color: var(--color-heading-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.soc-content .footnotes {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-subtle);
|
||||
}
|
||||
|
||||
/* ── Pager ── */
|
||||
|
||||
.soc-pager {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
.soc-pager-link {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
padding: 0.9rem 1.1rem;
|
||||
text-decoration: none;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.soc-pager-link.next {
|
||||
text-align: right;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.soc-pager-link:hover {
|
||||
border-color: var(--color-accent-border);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.soc-pager-label {
|
||||
color: var(--color-text-subtle);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.soc-pager-title {
|
||||
color: var(--color-heading-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Mobile ── */
|
||||
|
||||
.soc-menu-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.soc-shell {
|
||||
grid-template-columns: 230px minmax(0, 760px);
|
||||
}
|
||||
|
||||
.soc-toc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.soc-shell {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.soc-main {
|
||||
padding-top: 4.5rem;
|
||||
}
|
||||
|
||||
.soc-menu-toggle {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
z-index: 15;
|
||||
background: var(--color-surface-raised);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
color: var(--color-heading-secondary);
|
||||
font: inherit;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.soc-sidebar {
|
||||
position: fixed;
|
||||
inset: 0 30% 0 0;
|
||||
min-width: 260px;
|
||||
max-height: none;
|
||||
background: var(--color-bg);
|
||||
border-right: 1px solid var(--color-border);
|
||||
padding: 4.5rem 1.25rem 2rem;
|
||||
z-index: 10;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.soc-sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.soc-pager {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
||||
// "Add chips" — a rebuy, a top-up, or the first buy-in of the night.
|
||||
//
|
||||
// Quick amounts come off the table's starting stack, because a rebuy is almost
|
||||
// always one buy-in, sometimes half or two. The keypad is there for the times it
|
||||
// isn't. Recorded as cash in as well as chips, so at the end of the night the
|
||||
// dealer can say what each player actually handed over.
|
||||
const AddChips = {
|
||||
state: null,
|
||||
onConfirm: null,
|
||||
seatIndex: null,
|
||||
draft: "",
|
||||
|
||||
open(state, seatIndex, onConfirm) {
|
||||
this.state = state;
|
||||
this.seatIndex = seatIndex;
|
||||
this.onConfirm = onConfirm;
|
||||
this.draft = String(this.baseAmount());
|
||||
this.render();
|
||||
document.getElementById("addChipsOverlay").hidden = false;
|
||||
},
|
||||
|
||||
close() {
|
||||
document.getElementById("addChipsOverlay").hidden = true;
|
||||
},
|
||||
|
||||
// The table's starting stack if there is one, otherwise something sane to nudge
|
||||
// rather than an empty box.
|
||||
baseAmount() {
|
||||
const configured = Number(this.state.defaultStack);
|
||||
if (!Number.isNaN(configured) && configured > 0) return configured;
|
||||
const bb = Number(this.state.bigBlind) || 0;
|
||||
return bb > 0 ? bb * 100 : 100;
|
||||
},
|
||||
|
||||
quickAmounts() {
|
||||
const base = this.baseAmount();
|
||||
return [
|
||||
{ label: "½ buy-in", value: Money.round(base / 2) },
|
||||
{ label: "Buy-in", value: base },
|
||||
{ label: "2 buy-ins", value: base * 2 },
|
||||
];
|
||||
},
|
||||
|
||||
render() {
|
||||
const player = this.state.seats[this.seatIndex];
|
||||
document.getElementById("addChipsTitle").textContent =
|
||||
`Add chips — ${HoldemState.displayName(player, this.seatIndex)}`;
|
||||
|
||||
const stack = Betting.stackOf(this.state, player);
|
||||
const boughtIn = Number(player.buyIns) || 0;
|
||||
document.getElementById("addChipsSubtitle").textContent = boughtIn > 0
|
||||
? `${Money.format(stack)} in front · ${Money.format(boughtIn)} in so far`
|
||||
: `${Money.format(stack)} in front`;
|
||||
|
||||
const quick = document.getElementById("addChipsQuick");
|
||||
quick.innerHTML = "";
|
||||
this.quickAmounts().forEach(option => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "addChipsQuickBtn";
|
||||
btn.classList.toggle("selected", Number(this.draft) === option.value);
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.className = "addChipsQuickLabel";
|
||||
label.textContent = option.label;
|
||||
const amount = document.createElement("span");
|
||||
amount.className = "addChipsQuickAmount";
|
||||
amount.textContent = Money.format(option.value);
|
||||
|
||||
btn.appendChild(label);
|
||||
btn.appendChild(amount);
|
||||
btn.addEventListener("click", () => {
|
||||
this.draft = String(option.value);
|
||||
this.render();
|
||||
});
|
||||
quick.appendChild(btn);
|
||||
});
|
||||
|
||||
const input = document.getElementById("addChipsInput");
|
||||
input.value = this.draft;
|
||||
},
|
||||
|
||||
confirm() {
|
||||
const amount = Math.max(0, Number(document.getElementById("addChipsInput").value) || 0);
|
||||
this.close();
|
||||
if (amount > 0) this.onConfirm(this.seatIndex, Money.round(amount));
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = AddChips;
|
||||
}
|
||||
@@ -0,0 +1,801 @@
|
||||
// Live betting: who's to act, what it costs them, and where the money goes.
|
||||
//
|
||||
// This is bookkeeping, not a game — the dealer taps what the humans at the table
|
||||
// actually did, and the tool keeps the arithmetic straight. It deliberately never
|
||||
// blocks an action it thinks is "illegal"; a dealer overruling the maths is right
|
||||
// more often than the maths is.
|
||||
//
|
||||
// Money model: `player.committed` is what a seat has put in on the current street.
|
||||
// When a street closes it sweeps into `player.amountOverride`, which is the seat's
|
||||
// total for the hand — the same field the pot badge, the pot wizard, the standings
|
||||
// and the CSV already read. So the betting bar is a fast way to fill in a field
|
||||
// that already existed, and nothing downstream had to change.
|
||||
const Betting = {
|
||||
STREETS: ["preflop", "flop", "turn", "river"],
|
||||
|
||||
// How many community cards are face up once a street has been dealt. The flop is
|
||||
// three at once and everything after it is one at a time, which is why this is a
|
||||
// table rather than an index.
|
||||
BOARD_CARDS: { preflop: 0, flop: 3, turn: 4, river: 5 },
|
||||
|
||||
boardCardsFor(street) {
|
||||
return this.BOARD_CARDS[street] ?? 0;
|
||||
},
|
||||
|
||||
streetLabel(street) {
|
||||
return { preflop: "Pre-flop", flop: "Flop", turn: "Turn", river: "River" }[street] || "Pre-flop";
|
||||
},
|
||||
|
||||
round(amount) {
|
||||
return Money.round(amount);
|
||||
},
|
||||
|
||||
// ---- Reading the table ----
|
||||
|
||||
liveSeats(state) {
|
||||
return state.seats
|
||||
.map((player, index) => ({ player, index }))
|
||||
.filter(s => s.player.inHand && !s.player.folded);
|
||||
},
|
||||
|
||||
// Who would post if the hand were dealt right now. Used to preview the blinds
|
||||
// while the table is still being set, so a dealer can see where they'll land
|
||||
// before any money moves.
|
||||
// A seat that is dealt in, has nothing in the pot, and has no chips behind.
|
||||
//
|
||||
// Not the same as all-in: an all-in player has money riding on the hand. This is
|
||||
// somebody who busted and hasn't bought back in yet, which a home game produces
|
||||
// about once an hour. A seat with no stack *recorded* isn't broke either —
|
||||
// stackOf falls back to the table's starting stack for those — only an explicit
|
||||
// zero counts.
|
||||
brokeSeats(state) {
|
||||
const broke = [];
|
||||
state.seats.forEach((player, index) => {
|
||||
if (!player.inHand) return;
|
||||
if (this.handTotal(player) > this.EPSILON_CHIPS) return;
|
||||
if (this.stackOf(state, player) > this.EPSILON_CHIPS) return;
|
||||
broke.push(index);
|
||||
});
|
||||
return broke;
|
||||
},
|
||||
|
||||
// The table as it will be once the seats that can't post are dealt around. A view,
|
||||
// not a change: the preview has to ask the same question the deal will answer, and
|
||||
// asking it of a different shape of table is how the two came apart last time.
|
||||
tableAsDealt(state) {
|
||||
const broke = this.brokeSeats(state);
|
||||
if (broke.length === 0) return state;
|
||||
const skip = new Set(broke);
|
||||
return {
|
||||
...state,
|
||||
seats: state.seats.map((player, index) =>
|
||||
skip.has(index) ? { ...player, inHand: false, folded: false } : player),
|
||||
};
|
||||
},
|
||||
|
||||
blindPreview(state) {
|
||||
// Dealing relocates a button parked on an empty chair before it works out who
|
||||
// posts, so the preview has to answer for the button where it *will* be. Asking
|
||||
// from where it currently sits gave a different pair of blinds than the deal
|
||||
// produced — the preview promising one thing and doing another is exactly the
|
||||
// guessing this whole step exists to remove. Same reason the broke seats come
|
||||
// out first: the preview once promised a small blind from a seat with no chips.
|
||||
const dealt = this.tableAsDealt(state);
|
||||
const buttonIndex = HoldemState.buttonSeatForDeal(dealt);
|
||||
const asDealt = buttonIndex === dealt.dealerSeatIndex
|
||||
? dealt
|
||||
: { ...dealt, dealerSeatIndex: buttonIndex };
|
||||
|
||||
const satOut = this.brokeSeats(state);
|
||||
const { sbIndex, bbIndex, headsUp, manual, deadSmallBlind } = HoldemState.getBlindSeats(asDealt);
|
||||
if (bbIndex === null) return null;
|
||||
return {
|
||||
sbIndex, bbIndex, headsUp, manual: !!manual, deadSmallBlind: !!deadSmallBlind,
|
||||
deadButton: !!asDealt.deadButton && !asDealt.seats[buttonIndex].inHand,
|
||||
// Asked of the table as it will be dealt, for the same reason the blinds
|
||||
// are: someone sitting out for having no chips isn't owed a blind, and the
|
||||
// big blind of the hand about to go out isn't either.
|
||||
debtors: HoldemState.blindDebtors(asDealt, bbIndex),
|
||||
posting: (state.postingIn || []).slice(),
|
||||
buttonIndex,
|
||||
buttonMoves: buttonIndex !== state.dealerSeatIndex,
|
||||
satOut,
|
||||
smallBlind: this.round(state.smallBlind || 0),
|
||||
bigBlind: this.round(state.bigBlind || 0),
|
||||
// What the straddles will be if the hand is dealt as it stands, and how
|
||||
// many more this table could take.
|
||||
straddles: this.straddleSeats(state),
|
||||
maxStraddles: this.maxStraddles(state),
|
||||
};
|
||||
},
|
||||
|
||||
// ---- Straddles ----
|
||||
//
|
||||
// A straddle is a blind raise put out before the cards, by the player left of the
|
||||
// big blind, for twice it. It buys the last word before the flop: everyone else
|
||||
// acts first, and the straddler still has the option when it comes back round even
|
||||
// if nobody raised. Re-straddles carry on left, each doubling again.
|
||||
//
|
||||
// Home games run them constantly and a dealer has to get three things right at
|
||||
// once — the amount, who is first to act, and what the minimum raise becomes. The
|
||||
// tool used to do none of them, so a dealer's only way through was to deal, have
|
||||
// the straddler "raise" to $4, and hand the action to the wrong player.
|
||||
|
||||
// How many straddles this table could take. Everyone but the two blinds can put
|
||||
// one out; heads-up that is nobody, which is the right answer — the button is
|
||||
// already the small blind and there is no seat left of the big blind to straddle
|
||||
// from.
|
||||
maxStraddles(state) {
|
||||
return Math.max(0, this.liveSeats(this.tableAsDealt(state)).length - 2);
|
||||
},
|
||||
|
||||
// Who straddles and for how much, walking left from the big blind and doubling.
|
||||
// Asked of the table as it will be dealt, so a seat about to be sat out for having
|
||||
// no chips is never handed a straddle it can't post.
|
||||
straddleSeats(state, count = state.straddleCount) {
|
||||
const wanted = Math.min(Math.max(0, Number(count) || 0), this.maxStraddles(state));
|
||||
if (wanted <= 0) return [];
|
||||
|
||||
const dealt = this.tableAsDealt(state);
|
||||
const buttonIndex = HoldemState.buttonSeatForDeal(dealt);
|
||||
const asDealt = buttonIndex === dealt.dealerSeatIndex
|
||||
? dealt
|
||||
: { ...dealt, dealerSeatIndex: buttonIndex };
|
||||
const { bbIndex } = HoldemState.getBlindSeats(asDealt);
|
||||
if (bbIndex === null) return [];
|
||||
|
||||
const straddles = [];
|
||||
let index = bbIndex;
|
||||
let amount = this.round((Number(state.bigBlind) || 0) * 2);
|
||||
for (let n = 0; n < wanted; n++) {
|
||||
index = this.nextToAct(asDealt, index);
|
||||
if (index === null || index === bbIndex) break;
|
||||
straddles.push({ index, amount });
|
||||
amount = this.round(amount * 2);
|
||||
}
|
||||
return straddles;
|
||||
},
|
||||
|
||||
// Does handing this seat chips also put them back in the game?
|
||||
//
|
||||
// At a real table those are one event, and here they were two: a busted player got
|
||||
// their chips and stayed sat out, silently skipped hand after hand until somebody
|
||||
// noticed and found "Deal in" in the seat menu. Nothing said so — the toast just
|
||||
// read "Ben added $100".
|
||||
//
|
||||
// Not for a seat that was sat out while *holding* chips. That's a dealer who sat
|
||||
// somebody out on purpose and is topping them up, and dealing them back in would be
|
||||
// the tool guessing at something it wasn't told. Only a seat with no stack of its
|
||||
// own: busted at zero, or a chair being bought into for the first time.
|
||||
//
|
||||
// Deliberately reads player.stack rather than stackOf(), which substitutes the
|
||||
// table's starting stack for a seat that has none — the question here is what this
|
||||
// seat was actually recorded as holding.
|
||||
buyInDealsThemIn(player) {
|
||||
if (!player || player.inHand) return false;
|
||||
const own = player.stack;
|
||||
const recorded = own !== null && own !== undefined && String(own).trim() !== "";
|
||||
return !recorded || Number(own) <= this.EPSILON_CHIPS;
|
||||
},
|
||||
|
||||
// "bet" or "raise" — what putting more in is actually called right now.
|
||||
//
|
||||
// A wager into a pot nobody has wagered into is a bet; everything after that is a
|
||||
// raise. The bar decided this from what the acting player still owes, which gets
|
||||
// the big blind's option backwards: they owe nothing, so it offered "Bet", but
|
||||
// their own blind is a live bet and putting more in is a raise. It gets the other
|
||||
// half wrong too — the raise pad said "Raise to" for an opening bet on the flop,
|
||||
// and wrote "Ann's raise to $10" into the undo control for it.
|
||||
//
|
||||
// The arithmetic was always right (minRaiseTo returns two big blinds for the big
|
||||
// blind's option and one for an opening bet). This is only what it's called, which
|
||||
// for a tool a dealer announces the action from is not a small thing.
|
||||
wagerVerb(state) {
|
||||
return (state.currentBet || 0) > this.EPSILON_CHIPS ? "raise" : "bet";
|
||||
},
|
||||
|
||||
committed(player) {
|
||||
return this.round(player.committed || 0);
|
||||
},
|
||||
|
||||
// Everything this seat has put in across the whole hand, including the street
|
||||
// still in progress.
|
||||
handTotal(player) {
|
||||
return this.round((Number(player.amountOverride) || 0) + this.committed(player));
|
||||
},
|
||||
|
||||
// Never below zero. A seat can't have negative chips in front of it, and a typo in
|
||||
// the stack field used to come back as-is — which then flowed into putIn(), where
|
||||
// `Math.min(amount, -50)` moves a negative amount and *adds* chips to the table.
|
||||
stackOf(state, player) {
|
||||
const own = player.stack;
|
||||
if (own !== null && own !== undefined && String(own).trim() !== "") {
|
||||
return Math.max(0, this.round(own));
|
||||
}
|
||||
const fallback = Number(state.defaultStack);
|
||||
return Number.isNaN(fallback) ? 0 : Math.max(0, this.round(fallback));
|
||||
},
|
||||
|
||||
toCall(state, player) {
|
||||
return Math.max(0, this.round((state.currentBet || 0) - this.committed(player)));
|
||||
},
|
||||
|
||||
// A raise has to at least match the last one; before any raise, the big blind
|
||||
// is the yardstick.
|
||||
minRaiseTo(state) {
|
||||
const increment = this.round(state.lastRaiseSize || state.bigBlind || 0);
|
||||
return this.round((state.currentBet || 0) + (increment || 1));
|
||||
},
|
||||
|
||||
potSoFar(state) {
|
||||
return this.round(
|
||||
state.seats.reduce((sum, p) => sum + this.handTotal(p), 0)
|
||||
);
|
||||
},
|
||||
|
||||
// ---- Moving the action ----
|
||||
|
||||
nextLiveIndex(state, fromIndex) {
|
||||
const count = state.seatCount;
|
||||
for (let step = 1; step <= count; step++) {
|
||||
const index = this.seatAt(state, fromIndex, step);
|
||||
const player = state.seats[index];
|
||||
if (player.inHand && !player.folded) return index;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// Who the dealer asks next. A player who is all-in is still in the hand and still
|
||||
// wins pots, but has no decision left, so the action passes over them.
|
||||
nextToAct(state, fromIndex) {
|
||||
const count = state.seatCount;
|
||||
for (let step = 1; step <= count; step++) {
|
||||
const index = this.seatAt(state, fromIndex, step);
|
||||
const player = state.seats[index];
|
||||
if (player.inHand && !player.folded && this.stackOf(state, player) > this.EPSILON_CHIPS) return index;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// A player still in the hand with chips in the pot and nothing left behind. This is
|
||||
// the same stack test nextToAct uses to pass over them, which is the point: the
|
||||
// seat the felt marks all-in is exactly the seat the dealer will never be asked to
|
||||
// act on again.
|
||||
//
|
||||
// The "chips in the pot" half matters. A seat with an empty stack who hasn't put
|
||||
// anything in is a busted player who shouldn't have been dealt in, not a player
|
||||
// who moved all-in, and calling them all-in on the felt would be a lie.
|
||||
isAllIn(state, player) {
|
||||
if (!player || !player.inHand || player.folded) return false;
|
||||
return this.handTotal(player) > this.EPSILON_CHIPS &&
|
||||
this.stackOf(state, player) <= this.EPSILON_CHIPS;
|
||||
},
|
||||
|
||||
// Which chair sits `step` places along in the direction of play. The table owns
|
||||
// this — walking the seats is exactly where the button, the blinds and the action
|
||||
// used to drift apart from each other, so there is one implementation of it.
|
||||
seatAt(state, fromIndex, step) {
|
||||
return HoldemState.nextSeatIndex(state, fromIndex, step);
|
||||
},
|
||||
|
||||
// A street is done when everyone still live has either matched the bet or has
|
||||
// no chips left to argue with, and everyone has had a turn since the last raise.
|
||||
isStreetComplete(state) {
|
||||
const live = this.liveSeats(state);
|
||||
if (live.length <= 1) return true;
|
||||
|
||||
const unsettled = live.filter(s => {
|
||||
const allIn = this.stackOf(state, s.player) <= 0;
|
||||
const matched = this.toCall(state, s.player) <= 0;
|
||||
return !allIn && (!matched || !s.player.hasActed);
|
||||
});
|
||||
return unsettled.length === 0;
|
||||
},
|
||||
|
||||
// ---- Actions ----
|
||||
|
||||
// Moves `amount` from a seat's stack into the middle for this street. Returns
|
||||
// what actually moved, which is less than asked for if it puts them all-in.
|
||||
putIn(state, index, amount) {
|
||||
const player = state.seats[index];
|
||||
const stack = this.stackOf(state, player);
|
||||
const moved = this.round(Math.min(Math.max(0, amount), stack));
|
||||
|
||||
player.stack = this.round(stack - moved);
|
||||
player.committed = this.round(this.committed(player) + moved);
|
||||
return moved;
|
||||
},
|
||||
|
||||
fold(state, index) {
|
||||
state.seats[index].folded = true;
|
||||
state.seats[index].hasActed = true;
|
||||
this.passAction(state, index);
|
||||
},
|
||||
|
||||
check(state, index) {
|
||||
state.seats[index].hasActed = true;
|
||||
this.passAction(state, index);
|
||||
},
|
||||
|
||||
call(state, index) {
|
||||
const player = state.seats[index];
|
||||
this.putIn(state, index, this.toCall(state, player));
|
||||
player.hasActed = true;
|
||||
this.passAction(state, index);
|
||||
},
|
||||
|
||||
// `total` is what the seat's street commitment becomes — "raise to", the way a
|
||||
// dealer says it out loud, not "raise by".
|
||||
raiseTo(state, index, total) {
|
||||
const player = state.seats[index];
|
||||
const target = this.round(total);
|
||||
const delta = this.round(target - this.committed(player));
|
||||
const moved = this.putIn(state, index, delta);
|
||||
const newBet = this.committed(player);
|
||||
const previousBet = state.currentBet || 0;
|
||||
|
||||
if (newBet > previousBet) {
|
||||
const increase = this.round(newBet - previousBet);
|
||||
// Aggression, whether or not it was a full raise — a short all-in doesn't
|
||||
// reopen the betting, but the player who made it is still the last one who
|
||||
// put money in, and at the showdown they are still the one who shows first.
|
||||
state.streetAggressorIndex = index;
|
||||
state.handAggressorIndex = index;
|
||||
// What a full raise would have had to be: at least as much again as the
|
||||
// last one, or the big blind if there hasn't been a raise yet.
|
||||
const fullRaise = this.round(state.lastRaiseSize || state.bigBlind || 0);
|
||||
const isFullRaise = increase >= fullRaise - this.EPSILON_CHIPS;
|
||||
|
||||
state.currentBet = newBet;
|
||||
|
||||
if (isFullRaise) {
|
||||
state.lastRaiseSize = increase;
|
||||
// A real raise reopens the action: everyone else owes a response again.
|
||||
state.seats.forEach((p, i) => {
|
||||
if (i !== index && p.inHand && !p.folded) p.hasActed = false;
|
||||
});
|
||||
}
|
||||
// Otherwise this is a short all-in — somebody with less than a full raise
|
||||
// left shoving it in. The most misread rule in poker: it does *not* reopen
|
||||
// the betting to anyone who has already acted. They owe the difference or
|
||||
// they fold; they don't get to raise again because a short stack happened
|
||||
// to be between them. Players yet to act keep their full rights, which
|
||||
// they do here because their hasActed is already false.
|
||||
//
|
||||
// lastRaiseSize deliberately isn't lowered either, or the next legal raise
|
||||
// would be computed off the short amount and come out too small.
|
||||
}
|
||||
|
||||
player.hasActed = true;
|
||||
this.passAction(state, index);
|
||||
return moved;
|
||||
},
|
||||
|
||||
allIn(state, index) {
|
||||
const player = state.seats[index];
|
||||
return this.raiseTo(state, index, this.round(this.committed(player) + this.stackOf(state, player)));
|
||||
},
|
||||
|
||||
passAction(state, index) {
|
||||
state.actingSeatIndex = this.isStreetComplete(state) ? null : this.nextToAct(state, index);
|
||||
},
|
||||
|
||||
// ---- Street and hand boundaries ----
|
||||
|
||||
// Pulls whatever is in front of the players into their hand totals. Anything that
|
||||
// reads a seat's contribution — the pot wizard above all — has to run after this,
|
||||
// or it will price the pot without the chips sitting on the current street.
|
||||
sweepToHandTotals(state) {
|
||||
state.seats.forEach(player => {
|
||||
const moved = this.committed(player);
|
||||
if (moved > 0) {
|
||||
player.amountOverride = this.round((Number(player.amountOverride) || 0) + moved);
|
||||
}
|
||||
player.committed = 0;
|
||||
player.hasActed = false;
|
||||
});
|
||||
},
|
||||
|
||||
// Sweeps the street's chips into each seat's hand total and opens the next one.
|
||||
closeStreet(state) {
|
||||
this.sweepToHandTotals(state);
|
||||
|
||||
state.currentBet = 0;
|
||||
state.lastRaiseSize = 0;
|
||||
// Whoever led this street stops being the answer the moment the next one opens.
|
||||
// The hand's aggressor is deliberately left alone: it's the fallback for a
|
||||
// river nobody could bet on.
|
||||
state.streetAggressorIndex = null;
|
||||
|
||||
const nextStreetIndex = this.STREETS.indexOf(state.street || "preflop") + 1;
|
||||
state.street = this.STREETS[nextStreetIndex] || "river";
|
||||
|
||||
// It takes two players with chips to have a bet. Once everyone left is all-in
|
||||
// the rest of the board is just dealt out.
|
||||
if (!this.canStillBet(state)) {
|
||||
state.actingSeatIndex = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// After the flop the first live player left of the button acts — which heads-up
|
||||
// is the big blind, since the button is the small blind. Naming the big blind
|
||||
// directly also covers a button parked on a seat nobody is sitting in, and it
|
||||
// still has to be someone with chips left to bet.
|
||||
const { bbIndex, headsUp } = HoldemState.getBlindSeats(state);
|
||||
const bigBlindCanAct =
|
||||
headsUp &&
|
||||
bbIndex !== null &&
|
||||
!state.seats[bbIndex].folded &&
|
||||
this.stackOf(state, state.seats[bbIndex]) > this.EPSILON_CHIPS;
|
||||
|
||||
state.actingSeatIndex = bigBlindCanAct
|
||||
? bbIndex
|
||||
: this.nextToAct(state, state.dealerSeatIndex);
|
||||
},
|
||||
|
||||
isHandOver(state) {
|
||||
return this.liveSeats(state).length <= 1;
|
||||
},
|
||||
|
||||
// Players still in the hand who have chips left to put in. Fewer than two of them
|
||||
// means the betting is finished even though the hand isn't.
|
||||
seatsWithChips(state) {
|
||||
return this.liveSeats(state).filter(s => this.stackOf(state, s.player) > this.EPSILON_CHIPS);
|
||||
},
|
||||
|
||||
EPSILON_CHIPS: Money.EPSILON,
|
||||
|
||||
canStillBet(state) {
|
||||
return this.liveSeats(state).length > 1 && this.seatsWithChips(state).length > 1;
|
||||
},
|
||||
|
||||
// True when the hand is still live but nobody can bet again — the dealer is just
|
||||
// running the board out.
|
||||
isRunningOut(state) {
|
||||
// Only while a hand is actually being played. Before the deal, a table of
|
||||
// busted players who haven't rebought looks exactly like a table of all-ins to
|
||||
// this test, and the bar was announcing a run-out over a felt with no cards and
|
||||
// no chips on it.
|
||||
if (!state.bettingOpen) return false;
|
||||
return this.liveSeats(state).length > 1 && !this.canStillBet(state);
|
||||
},
|
||||
|
||||
// True while nothing has happened in this hand except the blinds going in. Up to
|
||||
// that point the dealer can still move the button or rename the blinds and have
|
||||
// it cost nothing — which is the state a home game spends a lot of time in,
|
||||
// arguing about whose turn it is before the cards come out.
|
||||
onlyBlindsIn(state) {
|
||||
return (state.street || "preflop") === "preflop" && state.seats.every(p => !p.hasActed);
|
||||
},
|
||||
|
||||
// Are the blinds actually down? Moving the button or naming a blind means two
|
||||
// completely different things depending on the answer, and `onlyBlindsIn` alone
|
||||
// cannot tell them apart: a table sitting in setup, with no cards out and nothing
|
||||
// committed, satisfies it just as well as a hand that has only just been dealt.
|
||||
//
|
||||
// With a hand open, the blinds are chips on the felt. Re-aiming them has to take
|
||||
// those chips back and post them again from the new positions, or the badges and
|
||||
// the money disagree.
|
||||
//
|
||||
// Before the deal it is a marker being slid round a table with nothing in the
|
||||
// middle. Re-posting there would *deal the hand* — betting opened, blinds taken off
|
||||
// two stacks, action on the seat after the big blind — which is not what a dealer
|
||||
// means by "she's the big blind this one". Setting the positions is how a dealer
|
||||
// gets ready to deal; the cards come out when they press the button, and not a
|
||||
// moment before.
|
||||
blindsArePosted(state) {
|
||||
return !!state.bettingOpen && this.onlyBlindsIn(state);
|
||||
},
|
||||
|
||||
// Chips the pot has been priced with but nobody has actually paid.
|
||||
//
|
||||
// Two ways a contribution gets into this tool. Tapping the action moves chips: the
|
||||
// bet leaves the stack, sits in front of the player, and is swept into their hand
|
||||
// total at the end of the street. The other way is the "without live betting" panel
|
||||
// — a base pot amount, or chips counted out on a seat — where the dealer is
|
||||
// recording a hand they are watching rather than driving, and nothing ever leaves
|
||||
// anybody's stack.
|
||||
//
|
||||
// The pot wizard prices both the same way, and awarding it credits the winner. So
|
||||
// in that second mode the table simply grew: measured at a three-handed table with
|
||||
// a $50 base pot, paying the pot took the chips on the table from $600 to $750 and
|
||||
// left the bank check blaming a rebuy that never happened.
|
||||
//
|
||||
// So before the pot is pushed, whatever was assumed rather than taken comes off the
|
||||
// stacks that assumed it. Chips already committed or swept are left alone; they
|
||||
// have been paid once already, and taking them twice is the same bug the other way
|
||||
// round.
|
||||
collectAssumedContributions(state) {
|
||||
const taken = [];
|
||||
state.seats.forEach((player, index) => {
|
||||
const priced = HoldemState.computeContribution(player, state.chipLegend, state.basePotAmount);
|
||||
const alreadyPaid = this.round(this.committed(player) + (Number(player.amountOverride) || 0));
|
||||
const owed = this.round(priced - alreadyPaid);
|
||||
if (owed <= this.EPSILON_CHIPS) return;
|
||||
|
||||
// Never more than they are holding. A seat priced above its own stack is a
|
||||
// dealer's typo or a player who is short, and inventing the difference is
|
||||
// what this whole function exists to stop.
|
||||
const moved = this.round(Math.min(owed, this.stackOf(state, player)));
|
||||
if (moved <= this.EPSILON_CHIPS) return;
|
||||
|
||||
player.stack = this.round(this.stackOf(state, player) - moved);
|
||||
// Recorded where a swept bet lives, so the money is in exactly the place
|
||||
// the rest of the tool looks for chips that are in the middle this hand.
|
||||
player.amountOverride = this.round((Number(player.amountOverride) || 0) + moved);
|
||||
taken.push({ index, name: HoldemState.displayName(player, index), amount: moved });
|
||||
});
|
||||
return { taken, total: this.round(taken.reduce((sum, row) => sum + row.amount, 0)) };
|
||||
},
|
||||
|
||||
// A misdeal: the hand never happened, so every chip goes back where it came from.
|
||||
//
|
||||
// The rulings sheet in this tool has told dealers to redeal since the day it was
|
||||
// written — first card to the wrong seat, a seat entitled to a hand dealt out, one
|
||||
// of the first two cards exposed — and the tool had no way to do it. The only
|
||||
// control that ended a live hand was Next Hand, which says out loud that it takes
|
||||
// the money off the table without paying anybody. That is right for an abandoned
|
||||
// pot and wrong for a misdeal, where the blinds and any calls belong to the people
|
||||
// who put them in.
|
||||
//
|
||||
// Undo covers the first second of it — "↶ dealing the hand" is one tap — but only
|
||||
// while the action history is still there. It lives in memory, so a dealer who
|
||||
// refreshes the page mid-hand, or who is three calls in, has nothing.
|
||||
//
|
||||
// What is deliberately kept: the button, the blinds, the straddle and anyone
|
||||
// posting in. A misdeal is the *same* hand dealt again, not the next one.
|
||||
takeHandBack(state) {
|
||||
const returned = [];
|
||||
state.seats.forEach((player, index) => {
|
||||
const back = this.round(this.committed(player) + (Number(player.amountOverride) || 0));
|
||||
if (back > 0) {
|
||||
player.stack = this.round(this.stackOf(state, player) + back);
|
||||
returned.push({ index, name: HoldemState.displayName(player, index), amount: back });
|
||||
}
|
||||
player.committed = 0;
|
||||
player.amountOverride = null;
|
||||
player.hasActed = false;
|
||||
player.folded = false;
|
||||
player.cards = ["", ""];
|
||||
});
|
||||
|
||||
state.bettingOpen = false;
|
||||
state.street = "preflop";
|
||||
state.currentBet = 0;
|
||||
state.lastRaiseSize = 0;
|
||||
state.actingSeatIndex = null;
|
||||
state.streetAggressorIndex = null;
|
||||
state.handAggressorIndex = null;
|
||||
HoldemState.clearBoard(state);
|
||||
// Nothing was posted, as far as the next rotation is concerned: the hand this
|
||||
// would have counted as is being dealt again from the same button.
|
||||
state.postedBlinds = null;
|
||||
state.postedStraddles = [];
|
||||
|
||||
return {
|
||||
returned,
|
||||
total: this.round(returned.reduce((sum, row) => sum + row.amount, 0)),
|
||||
};
|
||||
},
|
||||
|
||||
// Pulls the blinds back off the table and posts them again from wherever the
|
||||
// button and the blind assignment now say they belong. Refunding first is what
|
||||
// keeps the chip count straight when the button gets passed round three or four
|
||||
// times before anybody is happy with it.
|
||||
repostBlinds(state) {
|
||||
state.seats.forEach(player => {
|
||||
const back = this.committed(player);
|
||||
if (back > 0) player.stack = this.round(this.stackOf(state, player) + back);
|
||||
player.committed = 0;
|
||||
player.hasActed = false;
|
||||
});
|
||||
this.beginHand(state);
|
||||
},
|
||||
|
||||
// Deals a fresh betting round: blinds posted, action to the seat after the big
|
||||
// blind, which is where a dealer expects to be looking when the cards go out.
|
||||
//
|
||||
// Called when the dealer says the table is ready, not automatically when the
|
||||
// previous hand ends — everything before this point is setup, where seats and the
|
||||
// button can move freely because no money has been committed.
|
||||
beginHand(state) {
|
||||
state.bettingOpen = true;
|
||||
state.street = "preflop";
|
||||
state.currentBet = 0;
|
||||
state.lastRaiseSize = 0;
|
||||
// A fresh deal starts on an empty felt. Dealing without moving the button —
|
||||
// after a misdeal, or a table set up and dealt twice — would otherwise leave
|
||||
// the last hand's board sitting there being ranked against the new cards.
|
||||
HoldemState.clearBoard(state);
|
||||
// Posting a blind isn't aggression — a hand limped and checked all the way
|
||||
// down has no aggressor at all, which is exactly the case the showdown rule
|
||||
// sends to the seat left of the button.
|
||||
state.streetAggressorIndex = null;
|
||||
state.handAggressorIndex = null;
|
||||
|
||||
// Deal around anyone with no chips, before anything else reads the table.
|
||||
//
|
||||
// A busted player who hasn't rebought used to be dealt in anyway, and the
|
||||
// result was a hand nobody could finish: the table promised a small blind from
|
||||
// a seat with nothing to post it with, took it from nobody, put the action on
|
||||
// the big blind with only their own money in front of them, and announced
|
||||
// "3 players — all-in, running out the board" before a card was dealt.
|
||||
//
|
||||
// Sitting them out is what a dealer does. Seat, name and buy-in record are
|
||||
// untouched, so dealing them back in is one tap when they buy chips.
|
||||
this.brokeSeats(state).forEach(index => {
|
||||
state.seats[index].inHand = false;
|
||||
state.seats[index].folded = false;
|
||||
});
|
||||
|
||||
// Position is read off the button all through the hand, so it has to be on
|
||||
// somebody before the blinds are worked out — not on the empty seat 1 a
|
||||
// brand-new table starts on.
|
||||
HoldemState.ensureButtonOnPlayer(state);
|
||||
|
||||
state.seats.forEach(player => {
|
||||
player.committed = 0;
|
||||
player.hasActed = false;
|
||||
});
|
||||
|
||||
const sb = Number(state.smallBlind) || 0;
|
||||
const bb = Number(state.bigBlind) || 0;
|
||||
const { sbIndex, bbIndex, headsUp } = HoldemState.getBlindSeats(state);
|
||||
|
||||
// A null big blind means there aren't enough players to post at all. A null
|
||||
// small blind with a big blind behind it is the dead small blind, which is a
|
||||
// hand that gets dealt like any other — it just has one blind in it.
|
||||
if (bbIndex === null) {
|
||||
state.currentBet = 0;
|
||||
state.actingSeatIndex = null;
|
||||
state.postedBlinds = null;
|
||||
state.postedStraddles = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (sb > 0 && sbIndex !== null) this.putIn(state, sbIndex, sb);
|
||||
if (bb > 0) this.putIn(state, bbIndex, bb);
|
||||
|
||||
// Anyone the dealer said would post to come in past the blinds. It's a big
|
||||
// blind like any other — it buys them a hand they'd otherwise have to wait for
|
||||
// — so it goes in the middle the same way, before the count below reads it.
|
||||
const posting = Array.isArray(state.postingIn) ? state.postingIn : [];
|
||||
if (bb > 0) {
|
||||
state.seats.forEach((player, index) => {
|
||||
if (index === sbIndex || index === bbIndex) return;
|
||||
if (player.inHand && posting.includes(player.id)) this.putIn(state, index, bb);
|
||||
});
|
||||
}
|
||||
// Deliberately not cleared here. repostBlinds takes every posted chip back and
|
||||
// deals again through this same function, so a post that cleared itself on the
|
||||
// way in would be refunded and never retaken — the dealer moves the button and
|
||||
// somebody's post quietly vanishes. startNextHand is what ends it.
|
||||
|
||||
// What the next hand's rotation is worked out from. Recorded here, where the
|
||||
// blinds are actually posted, because by the time it's needed the seats have
|
||||
// moved on and the answer can no longer be read off the table.
|
||||
state.postedBlinds = { sbIndex, bbIndex };
|
||||
|
||||
// Who got cards. Read next hand to tell a player who has been here all along
|
||||
// from one who has just walked up and had the blinds go past them.
|
||||
state.dealtLastHand = state.seats.filter(p => p.inHand).map(p => p.id);
|
||||
|
||||
// Straddles go out after the blinds and before the cards, each one left of the
|
||||
// last. Posted here rather than left to the dealer to fake as a raise, because
|
||||
// a raise from that seat would hand the action to the wrong player.
|
||||
const straddles = this.straddleSeats(state);
|
||||
straddles.forEach(({ index, amount }) => this.putIn(state, index, amount));
|
||||
state.postedStraddles = straddles.map(({ index }) => index);
|
||||
|
||||
state.currentBet = this.round(state.seats.reduce(
|
||||
(most, player) => Math.max(most, this.committed(player)), 0));
|
||||
// A straddle is a blind raise, so what has to be matched to raise again is the
|
||||
// straddle itself, not the big blind underneath it: $2 blinds with a $4
|
||||
// straddle make the next raise $8, the same way the $2 blind makes it $4.
|
||||
// Left alone, the minimum would come out at $6 and the dealer would be talked
|
||||
// out of it by every player at the table.
|
||||
state.lastRaiseSize = straddles.length > 0 ? state.currentBet : (bb || 0);
|
||||
|
||||
// Heads-up the button is the small blind and acts first before the flop; with
|
||||
// three or more it's the seat after the big blind. Either way, a blind big
|
||||
// enough to put that player all-in leaves them nothing to decide.
|
||||
//
|
||||
// A straddle moves that seat along: first to act is left of the last straddler,
|
||||
// and the straddler keeps the option, which falls out of nobody having acted
|
||||
// yet.
|
||||
const lastPosted = straddles.length > 0
|
||||
? straddles[straddles.length - 1].index
|
||||
: bbIndex;
|
||||
state.actingSeatIndex = headsUp
|
||||
? (this.stackOf(state, state.seats[sbIndex]) > this.EPSILON_CHIPS ? sbIndex : this.nextToAct(state, sbIndex))
|
||||
: this.nextToAct(state, lastPosted);
|
||||
},
|
||||
|
||||
// What a pot-sized raise actually is, as a "raise to" total.
|
||||
//
|
||||
// Not "the bet plus the pot as it stands". The raiser calls first, and their call
|
||||
// is in the pot before the raise is measured off it, so the pot being matched is
|
||||
// the one that exists after the call:
|
||||
//
|
||||
// raise to = the current bet + (pot + what they owe) × fraction
|
||||
//
|
||||
// Pre-flop at 1/2 that gives $7, not $5: call the $2, which makes the pot $5, then
|
||||
// raise $5. Leaving the call out understates every one of these, and players say
|
||||
// "pot" out loud meaning a definite number.
|
||||
//
|
||||
// Facing no bet this is a plain pot-sized bet — toCall is zero and currentBet is
|
||||
// zero, so it comes out as the pot, which is the right answer there too.
|
||||
// Rounded up to something the table's chips can actually build. Half the pot is
|
||||
// constantly a half-chip amount — pre-flop at 1/2 it is $4.50, and the smallest
|
||||
// chip in the default legend is a dollar, so the button offered a number the
|
||||
// dealer would have to round themselves every single time.
|
||||
//
|
||||
// Only when the legend describes this game. A smallest chip *larger* than the big
|
||||
// blind means the denominations were never set for these stakes, and rounding a
|
||||
// 5c/10c table up to whole dollars would be the tool doing real damage on the
|
||||
// strength of a field somebody forgot to fill in. Left exact there, and the wrong
|
||||
// legend stays visible rather than being quietly applied.
|
||||
potRaiseTo(state, index, fraction = 1) {
|
||||
const player = state.seats[index];
|
||||
if (!player) return 0;
|
||||
const potAfterCall = this.round(this.potSoFar(state) + this.toCall(state, player));
|
||||
const exact = this.round((state.currentBet || 0) + potAfterCall * fraction);
|
||||
|
||||
const step = Money.fromCents(Money.chipStepCents(state.chipLegend));
|
||||
const bigBlind = Number(state.bigBlind) || 0;
|
||||
if (step <= 0 || (bigBlind > 0 && step > bigBlind)) return exact;
|
||||
return Money.upToChip(exact, state.chipLegend);
|
||||
},
|
||||
|
||||
// Who turns their hand over first.
|
||||
//
|
||||
// A call the dealer makes at every showdown that isn't a fold, and one nobody at
|
||||
// the table volunteers for — everyone would rather see a hand than show one. The
|
||||
// tool knew who last raised and who sits left of the button and said neither, so
|
||||
// the dealer was working it out from memory while five people waited.
|
||||
//
|
||||
// Three cases, in the order the rule asks them:
|
||||
// * somebody bet or raised on the final round — they show first;
|
||||
// * the round was checked through — first live player left of the button;
|
||||
// * nobody could bet, because the chips were already in — the last player who
|
||||
// did put money in, on whichever round that was.
|
||||
//
|
||||
// Returns null when there's nothing to call: one player left, or none.
|
||||
showdownOrder(state) {
|
||||
const live = this.liveSeats(state);
|
||||
if (live.length < 2) return null;
|
||||
|
||||
const seatOf = index =>
|
||||
Number.isInteger(index) && live.some(s => s.index === index) ? index : null;
|
||||
|
||||
const streetAggressor = seatOf(state.streetAggressorIndex);
|
||||
if (streetAggressor !== null) {
|
||||
return { index: streetAggressor, reason: "aggressor" };
|
||||
}
|
||||
|
||||
// Nobody could have bet this round, so "no bet on the final round" isn't a
|
||||
// choice anybody made and the seat left of the button hasn't earned the
|
||||
// obligation. It falls to whoever last put money in.
|
||||
if (!this.canStillBet(state)) {
|
||||
const handAggressor = seatOf(state.handAggressorIndex);
|
||||
if (handAggressor !== null) return { index: handAggressor, reason: "allInAggressor" };
|
||||
}
|
||||
|
||||
const order = HoldemState.liveOrderFromButton(state)
|
||||
.filter(index => live.some(s => s.index === index));
|
||||
return order.length > 0 ? { index: order[0], reason: "leftOfButton" } : null;
|
||||
},
|
||||
|
||||
// Winners get their share put back in front of them.
|
||||
creditWinnings(state, payouts) {
|
||||
payouts.forEach(payout => {
|
||||
const player = state.seats.find(p => p.id === payout.id);
|
||||
if (!player) return;
|
||||
player.stack = this.round(this.stackOf(state, player) + payout.amount);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = Betting;
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// Tap a card instead of typing it.
|
||||
//
|
||||
// Typing "AH" into a two-character box is fine at a desk and miserable at a table:
|
||||
// it needs a keyboard, it accepts nonsense like "ZZ", and it happily lets the same
|
||||
// card land in two players' hands. The picker is one tap per card, greys out cards
|
||||
// already dealt somewhere else at the table, and stores exactly the same two-character
|
||||
// string the rest of the tool has always read.
|
||||
const CardPicker = {
|
||||
RANKS: ["A", "K", "Q", "J", "T", "9", "8", "7", "6", "5", "4", "3", "2"],
|
||||
SUITS: [
|
||||
{ code: "S", symbol: "♠", red: false },
|
||||
{ code: "H", symbol: "♥", red: true },
|
||||
{ code: "D", symbol: "♦", red: true },
|
||||
{ code: "C", symbol: "♣", red: false },
|
||||
],
|
||||
|
||||
state: null,
|
||||
hooks: null,
|
||||
// Which set of slots is being filled: a seat's two hole cards, or the five
|
||||
// community cards. The grid, the duplicate check and the tap-to-fill behaviour are
|
||||
// the same job either way — what changes is where the card is written and how many
|
||||
// slots there are to walk along.
|
||||
target: null,
|
||||
seatIndex: null,
|
||||
cardIndex: 0,
|
||||
|
||||
open(state, hooks, seatIndex, cardIndex) {
|
||||
this.state = state;
|
||||
this.hooks = hooks;
|
||||
this.target = "seat";
|
||||
this.seatIndex = seatIndex;
|
||||
this.cardIndex = cardIndex;
|
||||
this.render();
|
||||
document.getElementById("cardPickerOverlay").hidden = false;
|
||||
},
|
||||
|
||||
// The board. `limit` is how many of the five are actually out — three on the flop,
|
||||
// four on the turn — so the sheet never offers a slot for a card the dealer has not
|
||||
// turned over yet.
|
||||
openBoard(state, hooks, cardIndex, limit = 5) {
|
||||
this.state = state;
|
||||
this.hooks = hooks;
|
||||
this.target = "board";
|
||||
this.seatIndex = null;
|
||||
this.boardLimit = Math.max(1, Math.min(5, limit));
|
||||
this.cardIndex = Math.min(cardIndex, this.boardLimit - 1);
|
||||
this.render();
|
||||
document.getElementById("cardPickerOverlay").hidden = false;
|
||||
},
|
||||
|
||||
onBoard() {
|
||||
return this.target === "board";
|
||||
},
|
||||
|
||||
// The slots the sheet is filling, whichever set that is.
|
||||
slotValues() {
|
||||
return this.onBoard()
|
||||
? (this.state.board || []).slice(0, this.boardLimit)
|
||||
: this.state.seats[this.seatIndex].cards;
|
||||
},
|
||||
|
||||
write(index, card) {
|
||||
if (this.onBoard()) {
|
||||
if (!Array.isArray(this.state.board)) this.state.board = ["", "", "", "", ""];
|
||||
this.state.board[index] = card;
|
||||
return;
|
||||
}
|
||||
this.state.seats[this.seatIndex].cards[index] = card;
|
||||
},
|
||||
|
||||
// Why the sheet is closing, because one caller needs to tell the difference.
|
||||
//
|
||||
// "filled" is the sheet finishing its own job — the last empty slot it was offering
|
||||
// got a card. "dismissed" is the dealer leaving: Done, Escape, or a tap on the scrim.
|
||||
// A single seat's cards don't care which. A showdown does: it walks the players one
|
||||
// at a time, and the two answers are "next player" and "stop asking".
|
||||
//
|
||||
// The hooks are dropped before the callback runs, so a caller that opens the sheet
|
||||
// again from inside `onDone` installs its own and this close can't fire twice.
|
||||
close(reason = "dismissed") {
|
||||
document.getElementById("cardPickerOverlay").hidden = true;
|
||||
const done = this.hooks && this.hooks.onDone;
|
||||
this.hooks = null;
|
||||
if (done) done(reason);
|
||||
},
|
||||
|
||||
// Every card already dealt somewhere — in front of a player or face up in the
|
||||
// middle. The slot being edited is excluded, so re-picking the card that's already
|
||||
// there isn't treated as a duplicate.
|
||||
//
|
||||
// The board belongs in here as much as the hands do, and in both directions: the
|
||||
// seven of clubs cannot be on the flop and in somebody's hand, whichever of the two
|
||||
// the dealer is filling in at the time.
|
||||
cardsInUse() {
|
||||
const used = new Map();
|
||||
this.state.seats.forEach((player, seat) => {
|
||||
(player.cards || []).forEach((card, slot) => {
|
||||
if (!card) return;
|
||||
if (!this.onBoard() && seat === this.seatIndex && slot === this.cardIndex) return;
|
||||
used.set(card.toUpperCase(), HoldemState.displayName(player, seat));
|
||||
});
|
||||
});
|
||||
(this.state.board || []).forEach((card, slot) => {
|
||||
if (!card) return;
|
||||
if (this.onBoard() && slot === this.cardIndex) return;
|
||||
used.set(card.toUpperCase(), "the board");
|
||||
});
|
||||
return used;
|
||||
},
|
||||
|
||||
assign(card) {
|
||||
this.write(this.cardIndex, card);
|
||||
this.hooks.onPick(this.seatIndex, this.cardIndex, card);
|
||||
|
||||
// Straight on to the next empty slot — a dealer picking hole cards is always
|
||||
// picking two and a flop is always three, and making them re-open the picker
|
||||
// between each one multiplies the taps by the only thing the sheet is for.
|
||||
const values = this.slotValues();
|
||||
const next = values.findIndex((value, i) => i > this.cardIndex && !value);
|
||||
if (next !== -1) {
|
||||
this.cardIndex = next;
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
this.close("filled");
|
||||
},
|
||||
|
||||
clearSlot() {
|
||||
this.write(this.cardIndex, "");
|
||||
this.hooks.onPick(this.seatIndex, this.cardIndex, "");
|
||||
this.render();
|
||||
},
|
||||
|
||||
// What this slot is called out loud: the flop's three cards, then the turn, then
|
||||
// the river — which is what a dealer is holding when they open this, rather than
|
||||
// "card 4".
|
||||
boardSlotLabel(slot) {
|
||||
return ["Flop 1", "Flop 2", "Flop 3", "Turn", "River"][slot] || `Card ${slot + 1}`;
|
||||
},
|
||||
|
||||
render() {
|
||||
const used = this.cardsInUse();
|
||||
const values = this.slotValues();
|
||||
|
||||
document.getElementById("cardPickerTitle").textContent = this.onBoard()
|
||||
? `Board — ${this.boardSlotLabel(this.cardIndex).toLowerCase()}`
|
||||
: `${HoldemState.displayName(this.state.seats[this.seatIndex], this.seatIndex)} — card ${this.cardIndex + 1}`;
|
||||
|
||||
// Which slot the next tap fills, switchable without closing the sheet.
|
||||
const slots = document.getElementById("cardPickerSlots");
|
||||
slots.innerHTML = "";
|
||||
values.forEach((_, slot) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "pickerSlot";
|
||||
btn.classList.toggle("active", slot === this.cardIndex);
|
||||
const value = values[slot] || "";
|
||||
btn.textContent = value || "—";
|
||||
if (this.onBoard()) btn.title = this.boardSlotLabel(slot);
|
||||
btn.classList.toggle("cardRed", /[HD]$/i.test(value));
|
||||
btn.addEventListener("click", () => {
|
||||
this.cardIndex = slot;
|
||||
this.render();
|
||||
});
|
||||
slots.appendChild(btn);
|
||||
});
|
||||
|
||||
const grid = document.getElementById("cardPickerGrid");
|
||||
grid.innerHTML = "";
|
||||
this.RANKS.forEach(rank => {
|
||||
this.SUITS.forEach(suit => {
|
||||
const card = `${rank}${suit.code}`;
|
||||
const takenBy = used.get(card);
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "pickerCard";
|
||||
btn.classList.toggle("cardRed", suit.red);
|
||||
btn.disabled = !!takenBy;
|
||||
if (takenBy) {
|
||||
btn.title = takenBy === "the board"
|
||||
? "Already face up on the board"
|
||||
: `Already dealt to ${takenBy}`;
|
||||
}
|
||||
if ((values[this.cardIndex] || "").toUpperCase() === card) {
|
||||
btn.classList.add("selected");
|
||||
}
|
||||
|
||||
const rankEl = document.createElement("span");
|
||||
rankEl.className = "pickerRank";
|
||||
rankEl.textContent = rank;
|
||||
const suitEl = document.createElement("span");
|
||||
suitEl.className = "pickerSuit";
|
||||
suitEl.textContent = suit.symbol;
|
||||
btn.appendChild(rankEl);
|
||||
btn.appendChild(suitEl);
|
||||
|
||||
btn.addEventListener("click", () => this.assign(card));
|
||||
grid.appendChild(btn);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = CardPicker;
|
||||
}
|
||||
@@ -46,7 +46,10 @@ const Toast = {
|
||||
|
||||
host.appendChild(toast);
|
||||
// Next frame, so the entrance transition actually runs.
|
||||
requestAnimationFrame(() => toast.classList.add("toastVisible"));
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add("toastVisible");
|
||||
this.publishHeight();
|
||||
});
|
||||
|
||||
this.timer = setTimeout(
|
||||
() => this.dismiss(),
|
||||
@@ -61,6 +64,24 @@ const Toast = {
|
||||
}
|
||||
const host = document.getElementById("toastHost");
|
||||
if (host) host.innerHTML = "";
|
||||
this.publishHeight();
|
||||
},
|
||||
|
||||
// How much of the bottom of the window this is standing on.
|
||||
//
|
||||
// The toast is fixed, so it is not in the flow and nothing scrolling a control into
|
||||
// view knows it is there — measured on the desktop sweep, tabbing to Common rulings
|
||||
// while a toast was up left the focus ring entirely behind it. The stylesheet adds
|
||||
// this to the bar's own height to work out where a focused control has to stop, so
|
||||
// it is published rather than guessed: the toast is one line or two depending on the
|
||||
// message, and on a narrow screen the same message is taller again.
|
||||
publishHeight() {
|
||||
if (typeof document === "undefined" || !document.documentElement) return;
|
||||
const host = document.getElementById("toastHost");
|
||||
const height = host && host.firstElementChild
|
||||
? Math.round(host.getBoundingClientRect().height)
|
||||
: 0;
|
||||
document.documentElement.style.setProperty("--toastHeight", `${height}px`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -15,8 +15,16 @@ const HandHistoryExport = {
|
||||
}
|
||||
},
|
||||
|
||||
toCsv(handHistory, standings = []) {
|
||||
const rows = [["Hand #", "Time", "Pot", "Payouts"]];
|
||||
// Money columns go in as bare numbers. Written as "$1,250,000.00" they import as
|
||||
// text, so the one thing anybody opens this file to do — total a column and see
|
||||
// who owes what — doesn't work without cleaning every cell first. The commas were
|
||||
// quoted correctly, so the file parsed; it just wouldn't add up.
|
||||
toCsv(handHistory, standings = [], { notes = "" } = {}) {
|
||||
// The rake gets its own column rather than being folded into the pot: the two
|
||||
// numbers answer different questions — what was in the middle, and what the
|
||||
// house took out of it — and a night's worth of the second one is what the box
|
||||
// is counted against.
|
||||
const rows = [["Hand #", "Time", "Pot", "House take", "Payouts"]];
|
||||
// handHistory is stored newest-first; the export reads better oldest-first.
|
||||
[...handHistory].reverse().forEach(entry => {
|
||||
const payouts = (entry.payouts || [])
|
||||
@@ -25,7 +33,8 @@ const HandHistoryExport = {
|
||||
rows.push([
|
||||
entry.handNumber,
|
||||
this.formatTimestamp(entry.timestamp),
|
||||
Money.format(entry.pot),
|
||||
Money.forSheet(entry.pot),
|
||||
Money.forSheet(entry.rake || 0),
|
||||
payouts,
|
||||
]);
|
||||
});
|
||||
@@ -35,10 +44,61 @@ const HandHistoryExport = {
|
||||
if (standings.length > 0) {
|
||||
rows.push([]);
|
||||
rows.push(["Session Standings"]);
|
||||
rows.push(["Player", "Paid In", "Won", "Net"]);
|
||||
// Bought in and chips now are what a cash-out is settled from; paid in and
|
||||
// won describe the hands. Both belong in the file the dealer hands over.
|
||||
//
|
||||
// Notes go last so the money columns keep the shape a spreadsheet expects.
|
||||
// "IOU: $20 to Ann" is exactly the kind of thing that has to be in the file
|
||||
// somebody settles up from, and it was only ever visible inside that seat's
|
||||
// own editor.
|
||||
rows.push(["Player", "Paid In", "Won", "Net", "Bought In", "Chips Now", "Up/Down", "Notes"]);
|
||||
standings.forEach(row => {
|
||||
rows.push([row.name, Money.format(row.in), Money.format(row.won), Money.formatSigned(row.net)]);
|
||||
rows.push([
|
||||
// Somebody who has gone home is marked in the file the same way the
|
||||
// panel marks them, because this is the copy that gets read at the
|
||||
// end of the night when nobody remembers who has already been paid.
|
||||
row.cashedOut ? `${row.name} (paid out)` : row.name,
|
||||
Money.forSheet(row.in),
|
||||
Money.forSheet(row.won),
|
||||
Money.forSheet(row.net),
|
||||
Money.forSheet(row.boughtIn || 0),
|
||||
Money.forSheet(row.chipsNow || 0),
|
||||
Money.forSheet(row.cashPosition || 0),
|
||||
row.notes || "",
|
||||
]);
|
||||
});
|
||||
|
||||
// And the answer the standings are read to get to. Whoever opens this file
|
||||
// wants the payment list, not to redo the arithmetic in a spreadsheet.
|
||||
const plan = SettleUp.plan(standings);
|
||||
if (plan.payments.length > 0) {
|
||||
rows.push([]);
|
||||
rows.push(["Settle Up"]);
|
||||
rows.push(["Pays", "Receives", "Amount"]);
|
||||
plan.payments.forEach(payment => {
|
||||
rows.push([payment.from, payment.to, Money.forSheet(payment.amount)]);
|
||||
});
|
||||
if (!plan.balanced) {
|
||||
// The file is the paper trail, so the caveat travels with it rather
|
||||
// than living only in a sidebar nobody exported.
|
||||
rows.push([
|
||||
plan.shortBy > 0
|
||||
? `Unpaid: ${Money.forSheet(plan.shortBy)} — more chips in play than cash taken`
|
||||
: `Unclaimed: ${Money.forSheet(plan.overBy)} — more cash taken than chips in play`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The table's own notes — side bets, IOUs, the promo running until midnight.
|
||||
// The dealer typed them because they matter to the night, and the export is the
|
||||
// thing that outlives the browser tab; leaving them behind meant the one place
|
||||
// the context was written down was the one place it couldn't be handed over.
|
||||
// csvField quotes the line breaks, so a multi-line note survives as one cell.
|
||||
if (notes && notes.trim()) {
|
||||
rows.push([]);
|
||||
rows.push(["Table Notes"]);
|
||||
rows.push([notes.trim()]);
|
||||
}
|
||||
|
||||
return rows.map(row => row.map(f => this.csvField(f)).join(",")).join("\r\n");
|
||||
@@ -51,8 +111,8 @@ const HandHistoryExport = {
|
||||
return `holdem-hand-history-${stamp}.csv`;
|
||||
},
|
||||
|
||||
download(handHistory, standings = []) {
|
||||
const csv = this.toCsv(handHistory, standings);
|
||||
download(handHistory, standings = [], options = {}) {
|
||||
const csv = this.toCsv(handHistory, standings, options);
|
||||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
// What beats what.
|
||||
//
|
||||
// The dealer has always been the one who decides who won: the pot wizard asks "who won
|
||||
// the hand?" and pays whoever gets ticked. That is the right default — most hands are
|
||||
// won without a showdown, and at a home game the table reads the cards out loud between
|
||||
// them. But when the board is on the felt and the hands are on their backs, the tool
|
||||
// knows the answer, and reading it off a screen beats three people counting kickers.
|
||||
//
|
||||
// Cards are the same two-character strings the rest of the tool has always stored:
|
||||
// a rank from `RANKS` and a suit from S, H, D, C. Case is not significant.
|
||||
//
|
||||
// Nothing in here touches the DOM, the table, or money. It compares cards, and the pot
|
||||
// wizard decides what to do about it.
|
||||
const HandRank = {
|
||||
RANKS: "23456789TJQKA",
|
||||
|
||||
// High to low. The number is the hand's category and nothing else — kickers and
|
||||
// the rest of the tie-breaking live in the `tiebreak` array beside it.
|
||||
STRAIGHT_FLUSH: 8,
|
||||
FOUR_OF_A_KIND: 7,
|
||||
FULL_HOUSE: 6,
|
||||
FLUSH: 5,
|
||||
STRAIGHT: 4,
|
||||
THREE_OF_A_KIND: 3,
|
||||
TWO_PAIR: 2,
|
||||
ONE_PAIR: 1,
|
||||
HIGH_CARD: 0,
|
||||
|
||||
// 2 through 14. Null for anything that isn't a card, which is how a half-filled
|
||||
// slot gets told apart from a bad one.
|
||||
rankValue(card) {
|
||||
if (typeof card !== "string" || card.length < 2) return null;
|
||||
const index = this.RANKS.indexOf(card[0].toUpperCase());
|
||||
return index < 0 ? null : index + 2;
|
||||
},
|
||||
|
||||
suitOf(card) {
|
||||
if (typeof card !== "string" || card.length < 2) return null;
|
||||
const suit = card[card.length - 1].toUpperCase();
|
||||
return "SHDC".includes(suit) ? suit : null;
|
||||
},
|
||||
|
||||
isCard(card) {
|
||||
return this.rankValue(card) !== null && this.suitOf(card) !== null;
|
||||
},
|
||||
|
||||
// Singular and plural, for reading a hand out loud.
|
||||
NAMES: {
|
||||
14: ["ace", "aces"], 13: ["king", "kings"], 12: ["queen", "queens"],
|
||||
11: ["jack", "jacks"], 10: ["ten", "tens"], 9: ["nine", "nines"],
|
||||
8: ["eight", "eights"], 7: ["seven", "sevens"], 6: ["six", "sixes"],
|
||||
5: ["five", "fives"], 4: ["four", "fours"], 3: ["three", "threes"],
|
||||
2: ["two", "twos"],
|
||||
},
|
||||
|
||||
name(rank, plural = false) {
|
||||
const pair = this.NAMES[rank];
|
||||
return pair ? pair[plural ? 1 : 0] : "?";
|
||||
},
|
||||
|
||||
// ---- The five-card hand -------------------------------------------------------
|
||||
|
||||
// Exactly five cards, ranked. Everything else is built on this.
|
||||
//
|
||||
// Five rather than seven because five is the hand a player actually holds, and it
|
||||
// is small enough to check by hand — which is the whole argument for doing it this
|
||||
// way. The seven-card version below tries all twenty-one five-card hands and keeps
|
||||
// the best, which is a hand of poker's own definition of "your best five" written
|
||||
// out literally. A dealer resolves one pot a hand with at most a dozen players in
|
||||
// it; two hundred and fifty comparisons of five cards is not a cost worth trading
|
||||
// a subtler algorithm's correctness for.
|
||||
evaluate5(cards) {
|
||||
if (!Array.isArray(cards) || cards.length !== 5) return null;
|
||||
if (!cards.every(card => this.isCard(card))) return null;
|
||||
|
||||
const ranks = cards.map(card => this.rankValue(card)).sort((a, b) => b - a);
|
||||
const suits = cards.map(card => this.suitOf(card));
|
||||
const flush = suits.every(suit => suit === suits[0]);
|
||||
|
||||
// How many of each rank, ordered by how many first and by rank second — so
|
||||
// the front of this list is always the rank that decides the hand.
|
||||
const counts = new Map();
|
||||
ranks.forEach(rank => counts.set(rank, (counts.get(rank) || 0) + 1));
|
||||
const groups = [...counts.entries()]
|
||||
.sort((a, b) => (b[1] - a[1]) || (b[0] - a[0]));
|
||||
const shape = groups.map(([, count]) => count);
|
||||
const byGroup = groups.map(([rank]) => rank);
|
||||
|
||||
const straightHigh = this.straightHigh(ranks);
|
||||
|
||||
const made = (category, tiebreak) => ({ category, tiebreak, cards: [...cards] });
|
||||
|
||||
if (flush && straightHigh) return made(this.STRAIGHT_FLUSH, [straightHigh]);
|
||||
if (shape[0] === 4) return made(this.FOUR_OF_A_KIND, byGroup);
|
||||
if (shape[0] === 3 && shape[1] === 2) return made(this.FULL_HOUSE, byGroup);
|
||||
if (flush) return made(this.FLUSH, ranks);
|
||||
if (straightHigh) return made(this.STRAIGHT, [straightHigh]);
|
||||
if (shape[0] === 3) return made(this.THREE_OF_A_KIND, byGroup);
|
||||
if (shape[0] === 2 && shape[1] === 2) return made(this.TWO_PAIR, byGroup);
|
||||
if (shape[0] === 2) return made(this.ONE_PAIR, byGroup);
|
||||
return made(this.HIGH_CARD, ranks);
|
||||
},
|
||||
|
||||
// The top card of the straight these five ranks make, or null.
|
||||
//
|
||||
// The wheel is the exception the rest of the ordering has to be told about: A-2-3-4-5
|
||||
// is a straight, and it is the *lowest* one, so its ace counts as five rather than
|
||||
// as fourteen. Getting that wrong is how A-2-3-4-5 comes to beat 10-J-Q-K-A.
|
||||
straightHigh(sortedDesc) {
|
||||
const distinct = [...new Set(sortedDesc)];
|
||||
if (distinct.length !== 5) return null;
|
||||
if (distinct[0] - distinct[4] === 4) return distinct[0];
|
||||
// Ace, five, four, three, two.
|
||||
if (distinct[0] === 14 && distinct[1] === 5 && distinct[4] === 2) return 5;
|
||||
return null;
|
||||
},
|
||||
|
||||
// ---- The best five out of however many ----------------------------------------
|
||||
|
||||
// The best five-card hand available from five, six or seven cards — hole cards and
|
||||
// board together, which is what Hold'em asks for. Returns null if there aren't five
|
||||
// real cards, which is the ordinary state of a hand still being dealt.
|
||||
best(cards) {
|
||||
const real = (cards || []).filter(card => this.isCard(card));
|
||||
if (real.length < 5) return null;
|
||||
// A card in two places at once is a mis-entry, not a hand — say so rather than
|
||||
// ranking a board that has the same king on it twice.
|
||||
if (new Set(real.map(card => card.toUpperCase())).size !== real.length) return null;
|
||||
|
||||
let best = null;
|
||||
this.eachFive(real, five => {
|
||||
const hand = this.evaluate5(five);
|
||||
if (!best || this.compare(hand, best) > 0) best = hand;
|
||||
});
|
||||
return best;
|
||||
},
|
||||
|
||||
// Every five-card subset, without building an array of them.
|
||||
eachFive(cards, visit) {
|
||||
const n = cards.length;
|
||||
for (let a = 0; a < n - 4; a++) {
|
||||
for (let b = a + 1; b < n - 3; b++) {
|
||||
for (let c = b + 1; c < n - 2; c++) {
|
||||
for (let d = c + 1; d < n - 1; d++) {
|
||||
for (let e = d + 1; e < n; e++) {
|
||||
visit([cards[a], cards[b], cards[c], cards[d], cards[e]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Positive if `a` wins, negative if `b` does, zero for a chop. A chop is a real
|
||||
// answer here rather than a rounding of one: two players playing the same board
|
||||
// split the pot, and the fifth card decides it far more often than a table expects.
|
||||
compare(a, b) {
|
||||
if (!a && !b) return 0;
|
||||
if (!a) return -1;
|
||||
if (!b) return 1;
|
||||
if (a.category !== b.category) return a.category - b.category;
|
||||
const length = Math.max(a.tiebreak.length, b.tiebreak.length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
const diff = (a.tiebreak[i] || 0) - (b.tiebreak[i] || 0);
|
||||
if (diff !== 0) return diff;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
// ---- Saying it out loud -------------------------------------------------------
|
||||
|
||||
// What a dealer says when they turn the hand over. Named to the level that settles
|
||||
// it and no further: "two pair, aces and eights" is the call, and the kicker only
|
||||
// gets a mention where the tool has had to use one to break a tie.
|
||||
describe(hand) {
|
||||
if (!hand) return "";
|
||||
const [first, second] = hand.tiebreak;
|
||||
switch (hand.category) {
|
||||
case this.STRAIGHT_FLUSH:
|
||||
return first === 14 ? "Royal flush" : `Straight flush, ${this.name(first)} high`;
|
||||
case this.FOUR_OF_A_KIND:
|
||||
return `Four of a kind, ${this.name(first, true)}`;
|
||||
case this.FULL_HOUSE:
|
||||
return `Full house, ${this.name(first, true)} full of ${this.name(second, true)}`;
|
||||
case this.FLUSH:
|
||||
return `Flush, ${this.name(first)} high`;
|
||||
case this.STRAIGHT:
|
||||
return `Straight, ${this.name(first)} high`;
|
||||
case this.THREE_OF_A_KIND:
|
||||
return `Three of a kind, ${this.name(first, true)}`;
|
||||
case this.TWO_PAIR:
|
||||
return `Two pair, ${this.name(first, true)} and ${this.name(second, true)}`;
|
||||
case this.ONE_PAIR:
|
||||
return `Pair of ${this.name(first, true)}`;
|
||||
default:
|
||||
return `${this.name(first).charAt(0).toUpperCase()}${this.name(first).slice(1)} high`;
|
||||
}
|
||||
},
|
||||
|
||||
// The same call with the article a table would put in front of it. Not derivable
|
||||
// from the first letter: it is "a full house" and "a flush", but "four of a kind"
|
||||
// and "two pair" take none at all, and "an ace high" is not something anybody says.
|
||||
// So it belongs to the category, like the name does.
|
||||
ARTICLED: {
|
||||
8: hand => (hand.tiebreak[0] === 14 ? "a royal flush" : "a"),
|
||||
7: () => null,
|
||||
6: () => "a",
|
||||
5: () => "a",
|
||||
4: () => "a",
|
||||
3: () => null,
|
||||
2: () => null,
|
||||
1: () => "a",
|
||||
0: () => null,
|
||||
},
|
||||
|
||||
// "wins with X" — the phrase, ready to drop into a sentence.
|
||||
phrase(hand) {
|
||||
if (!hand) return "the best hand";
|
||||
const described = this.describe(hand);
|
||||
const article = (this.ARTICLED[hand.category] || (() => null))(hand);
|
||||
if (article === null) return described.toLowerCase();
|
||||
if (article !== "a") return article;
|
||||
return `a ${described.toLowerCase()}`;
|
||||
},
|
||||
|
||||
// ---- The showdown -------------------------------------------------------------
|
||||
|
||||
// Rank a set of players against a board.
|
||||
//
|
||||
// `players` is [{ id, cards }, ...] — the two in front of each of them. Every player
|
||||
// is returned whether or not they could be ranked, in the order they were given, so
|
||||
// the caller can show a row for each and say which ones it could not read. `place`
|
||||
// is 1 for the winner and shared by a chop; a player whose cards aren't known has a
|
||||
// null hand and no place.
|
||||
//
|
||||
// `complete` is the question the pot wizard actually asks: can this be trusted to
|
||||
// pay the table. It is only true when the board is all five cards and every single
|
||||
// player handed in has two of their own — one unknown hand and the tool cannot say
|
||||
// who won, only who is winning so far, which is not the same thing and must not be
|
||||
// dressed up as it.
|
||||
showdown(board, players) {
|
||||
const boardCards = (board || []).filter(card => this.isCard(card));
|
||||
const boardComplete = boardCards.length === 5;
|
||||
|
||||
const ranked = (players || []).map(player => {
|
||||
const hole = (player.cards || []).filter(card => this.isCard(card));
|
||||
const hand = hole.length === 2 && boardComplete
|
||||
? this.best([...hole, ...boardCards])
|
||||
: null;
|
||||
return { id: player.id, cards: hole, hand, place: null };
|
||||
});
|
||||
|
||||
const readable = ranked.filter(row => row.hand);
|
||||
// A duplicated card anywhere means somebody has been mis-entered, and the
|
||||
// ranking would be of a deck that doesn't exist.
|
||||
const all = [...boardCards, ...ranked.flatMap(row => row.cards)].map(c => c.toUpperCase());
|
||||
const duplicated = new Set(all).size !== all.length;
|
||||
|
||||
const sorted = [...readable].sort((a, b) => this.compare(b.hand, a.hand));
|
||||
let place = 0;
|
||||
sorted.forEach((row, i) => {
|
||||
if (i === 0 || this.compare(row.hand, sorted[i - 1].hand) !== 0) place = i + 1;
|
||||
row.place = place;
|
||||
});
|
||||
|
||||
const winners = sorted.filter(row => row.place === 1);
|
||||
const complete = boardComplete && !duplicated &&
|
||||
ranked.length > 0 && readable.length === ranked.length;
|
||||
|
||||
return {
|
||||
players: ranked,
|
||||
winners: complete ? winners.map(row => row.id) : [],
|
||||
boardComplete,
|
||||
duplicated,
|
||||
complete,
|
||||
// Who the tool could not read, so the dialog can name them rather than
|
||||
// just declining to help.
|
||||
unknown: ranked.filter(row => !row.hand).map(row => row.id),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = HandRank;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,147 @@
|
||||
// Keyboard and screen-reader behaviour for every overlay in the tool, in one place.
|
||||
//
|
||||
// There are six of them — seat menu, card picker, add chips, rulings, pot wizard,
|
||||
// confirm — opened and closed by five different modules, and each one was missing
|
||||
// the same three things: focus never moved into the dialog, Tab walked straight out
|
||||
// of it into the page behind the scrim, and Escape did nothing (only the rulings
|
||||
// sheet handled it). Measured before this: 0 of 5 moved focus in, 5 of 5 leaked Tab,
|
||||
// 1 of 5 closed on Escape.
|
||||
//
|
||||
// Rather than patch that into five modules, this watches the `hidden` attribute every
|
||||
// overlay already toggles. Nothing else had to change, and an overlay added later
|
||||
// gets the same behaviour without anyone remembering to wire it up — which is the
|
||||
// failure this is fixing in the first place.
|
||||
const Modal = {
|
||||
// `:not([hidden])` matters — a sheet that is itself hidden inside an open overlay
|
||||
// must not be a Tab stop.
|
||||
FOCUSABLE: [
|
||||
"a[href]",
|
||||
"button:not([disabled])",
|
||||
'input:not([disabled]):not([type="hidden"])',
|
||||
"select:not([disabled])",
|
||||
"textarea:not([disabled])",
|
||||
'[tabindex]:not([tabindex="-1"])',
|
||||
].join(","),
|
||||
|
||||
open: [],
|
||||
|
||||
mount() {
|
||||
document.querySelectorAll('[id$="Overlay"]').forEach(overlay => this.watch(overlay));
|
||||
// Capture phase: the dialog gets first refusal on Escape and Tab, before any
|
||||
// table-level shortcut sees them.
|
||||
document.addEventListener("keydown", e => this.onKeyDown(e), true);
|
||||
},
|
||||
|
||||
watch(overlay) {
|
||||
const sync = () => {
|
||||
const isOpen = !overlay.hidden;
|
||||
const known = this.open.some(entry => entry.overlay === overlay);
|
||||
if (isOpen && !known) this.onOpened(overlay);
|
||||
else if (!isOpen && known) this.onClosed(overlay);
|
||||
};
|
||||
new MutationObserver(sync).observe(overlay, { attributes: true, attributeFilter: ["hidden"] });
|
||||
sync();
|
||||
},
|
||||
|
||||
sheetOf(overlay) {
|
||||
return overlay.querySelector('[role="dialog"], [role="alertdialog"]') || overlay.firstElementChild || overlay;
|
||||
},
|
||||
|
||||
focusableIn(overlay) {
|
||||
return [...overlay.querySelectorAll(this.FOCUSABLE)].filter(el => {
|
||||
if (el.closest("[hidden]")) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
});
|
||||
},
|
||||
|
||||
onOpened(overlay) {
|
||||
const sheet = this.sheetOf(overlay);
|
||||
// aria-modal is only true while the thing is actually open, so it's set here
|
||||
// rather than baked into the markup.
|
||||
if (sheet) sheet.setAttribute("aria-modal", "true");
|
||||
|
||||
this.open.push({ overlay, returnFocus: document.activeElement });
|
||||
|
||||
// Focus the first real control. A dialog that leaves focus on the button
|
||||
// behind it reads, to a screen reader, as though nothing happened at all.
|
||||
// Content-only sheets get focus on the sheet so the title is announced.
|
||||
const focusable = this.focusableIn(overlay);
|
||||
const target = focusable[0];
|
||||
if (target) {
|
||||
target.focus({ preventScroll: true });
|
||||
} else if (sheet) {
|
||||
if (!sheet.hasAttribute("tabindex")) sheet.setAttribute("tabindex", "-1");
|
||||
sheet.focus({ preventScroll: true });
|
||||
}
|
||||
},
|
||||
|
||||
onClosed(overlay) {
|
||||
const index = this.open.findIndex(entry => entry.overlay === overlay);
|
||||
if (index === -1) return;
|
||||
const [entry] = this.open.splice(index, 1);
|
||||
|
||||
const sheet = this.sheetOf(overlay);
|
||||
if (sheet) sheet.removeAttribute("aria-modal");
|
||||
|
||||
// Put the dealer back where they were. Without this, closing the pot wizard
|
||||
// drops focus to the top of the document and the next Tab starts from the
|
||||
// page header.
|
||||
const back = entry.returnFocus;
|
||||
if (back && back.isConnected && typeof back.focus === "function") {
|
||||
back.focus({ preventScroll: true });
|
||||
}
|
||||
},
|
||||
|
||||
topmost() {
|
||||
return this.open.length ? this.open[this.open.length - 1] : null;
|
||||
},
|
||||
|
||||
// Escape presses the dialog's own cancel/close button rather than just hiding the
|
||||
// overlay, so whatever that button does — resolving the confirm promise, clearing
|
||||
// the add-chips draft — still happens. Only if there isn't one does it hide the
|
||||
// overlay directly.
|
||||
dismiss(overlay) {
|
||||
const closer = overlay.querySelector("[data-modal-close]");
|
||||
if (closer) closer.click();
|
||||
else overlay.hidden = true;
|
||||
},
|
||||
|
||||
onKeyDown(event) {
|
||||
const top = this.topmost();
|
||||
if (!top) return;
|
||||
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.dismiss(top.overlay);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key !== "Tab") return;
|
||||
|
||||
// Keep Tab inside the dialog. Everything behind the scrim is unreachable by
|
||||
// mouse, so letting the keyboard wander back there is a trap with no way out.
|
||||
const focusable = this.focusableIn(top.overlay);
|
||||
if (focusable.length === 0) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
const active = document.activeElement;
|
||||
|
||||
if (event.shiftKey && (active === first || !top.overlay.contains(active))) {
|
||||
event.preventDefault();
|
||||
last.focus({ preventScroll: true });
|
||||
} else if (!event.shiftKey && (active === last || !top.overlay.contains(active))) {
|
||||
event.preventDefault();
|
||||
first.focus({ preventScroll: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = Modal;
|
||||
}
|
||||
@@ -1,10 +1,182 @@
|
||||
// Shared money formatting for every casino surface. Kept apart from the table's
|
||||
// rendering so the quick-split calculator can pull in the formatter without dragging
|
||||
// the whole seat/chip UI along with it.
|
||||
// Shared money handling for every casino surface. Kept apart from the table's
|
||||
// rendering so the quick-split calculator can pull it in without dragging the whole
|
||||
// seat/chip UI along with it.
|
||||
//
|
||||
// Everything the dealer touches is money, and money is decimal, so every module used
|
||||
// to carry its own `Math.round(x * 100) / 100`. That's one rounding rule written out
|
||||
// sixteen times in six files, which is sixteen chances for one of them to drift.
|
||||
// It lives here now, and the modules that do arithmetic call these instead.
|
||||
const Money = {
|
||||
// Half a cent. Chip comparisons need a tolerance: a stack that lands on
|
||||
// 0.0000000001 after a split is empty as far as a dealer is concerned, and an
|
||||
// exact `> 0` would treat that player as still having chips to bet.
|
||||
EPSILON: 0.004,
|
||||
|
||||
// All money maths goes through cents, so 0.1 + 0.2 never reaches a payout.
|
||||
toCents(amount) {
|
||||
return Math.round((Number(amount) || 0) * 100);
|
||||
},
|
||||
|
||||
fromCents(cents) {
|
||||
return cents / 100;
|
||||
},
|
||||
|
||||
round(amount) {
|
||||
return this.toCents(amount) / 100;
|
||||
},
|
||||
|
||||
// Cents only when there are cents.
|
||||
//
|
||||
// A chip game deals in whole numbers nearly all the time, and a trailing ".00"
|
||||
// is two characters that say nothing — measured at ~22px, against the 64px a
|
||||
// seat puck has to show a stack in. "$1,250,000.00" doesn't fit and wraps;
|
||||
// "$1,250,000" does. Amounts that genuinely carry cents still show them, so a
|
||||
// 25c-blind game reads exactly as it did.
|
||||
format(amount) {
|
||||
const value = Number(amount) || 0;
|
||||
return `$${value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
const hasCents = this.toCents(value) % 100 !== 0;
|
||||
return `$${value.toLocaleString(undefined, {
|
||||
minimumFractionDigits: hasCents ? 2 : 0,
|
||||
maximumFractionDigits: 2,
|
||||
})}`;
|
||||
},
|
||||
|
||||
// The same amount for a spreadsheet rather than for a person: no currency symbol
|
||||
// and no thousands separators, because those are what make a column of money
|
||||
// import as text and refuse to add up.
|
||||
forSheet(amount) {
|
||||
return (this.toCents(amount) / 100).toFixed(2);
|
||||
},
|
||||
|
||||
// The chips to actually push, for an amount.
|
||||
//
|
||||
// A payout figure is the answer to the arithmetic, not to the dealer's job: they
|
||||
// still have to work out that $132 is two blacks, a green, a red and two whites
|
||||
// while six people watch. The chip legend already knows the denominations.
|
||||
//
|
||||
// Largest first, which is greedy and therefore not optimal for an arbitrary set of
|
||||
// values — but poker chips are not arbitrary. Every real set is canonical (each
|
||||
// denomination divides the next: 1, 5, 25, 100, 500), and greedy is exactly optimal
|
||||
// on those. A dealer who invents a 3-and-7 chip set gets a valid answer that might
|
||||
// use one chip more than it had to.
|
||||
//
|
||||
// Anything the smallest chip can't make comes back as `remainder` rather than being
|
||||
// rounded away: a 50c share in a game whose smallest chip is a dollar is a real
|
||||
// problem for the dealer to solve, not a rounding error to hide.
|
||||
// The smallest chip the table has said it plays with, in cents. Zero when no
|
||||
// denominations are set, which means the tool has been told nothing about what
|
||||
// chips exist and shouldn't pretend otherwise.
|
||||
chipStepCents(legend) {
|
||||
const values = (legend || [])
|
||||
.map(chip => this.toCents(chip.value))
|
||||
.filter(cents => cents > 0);
|
||||
return values.length > 0 ? Math.min(...values) : 0;
|
||||
},
|
||||
|
||||
// Rounds an amount up to something the table can actually build out of its chips.
|
||||
//
|
||||
// Up rather than down or nearest: these are bet sizes, and a pot-sized bet that
|
||||
// has been shaved is no longer a pot-sized bet. Rounding up also can't drop an
|
||||
// amount below a legal minimum, which rounding the other way can.
|
||||
upToChip(amount, legend) {
|
||||
const step = this.chipStepCents(legend);
|
||||
if (step <= 0) return this.round(amount);
|
||||
const cents = this.toCents(amount);
|
||||
if (cents <= 0) return this.round(amount);
|
||||
return this.fromCents(Math.ceil(cents / step) * step);
|
||||
},
|
||||
|
||||
// And down, for money coming *out* of a pot rather than going into it.
|
||||
//
|
||||
// The rake is the one amount in this tool that leaves the table, so the direction
|
||||
// of the rounding is a decision about whose money it is. Down: a house that can't
|
||||
// make 40c out of its chips takes nothing rather than a dollar, and the players
|
||||
// keep the difference. Being wrong in the players' favour is the only way to be
|
||||
// wrong about this that nobody has to argue about.
|
||||
downToChip(amount, legend) {
|
||||
const step = this.chipStepCents(legend);
|
||||
if (step <= 0) return this.round(amount);
|
||||
const cents = this.toCents(amount);
|
||||
if (cents <= 0) return 0;
|
||||
return this.fromCents(Math.floor(cents / step) * step);
|
||||
},
|
||||
|
||||
chipBreakdown(amount, legend) {
|
||||
let cents = this.toCents(amount);
|
||||
if (cents <= 0) return { chips: [], remainder: 0 };
|
||||
|
||||
const values = (legend || [])
|
||||
.map(chip => ({ chip, cents: this.toCents(chip.value) }))
|
||||
.filter(entry => entry.cents > 0)
|
||||
.sort((a, b) => b.cents - a.cents);
|
||||
|
||||
const chips = [];
|
||||
values.forEach(entry => {
|
||||
const count = Math.floor(cents / entry.cents);
|
||||
if (count <= 0) return;
|
||||
cents -= count * entry.cents;
|
||||
chips.push({ value: this.fromCents(entry.cents), count, color: entry.chip.color });
|
||||
});
|
||||
|
||||
return { chips, remainder: this.fromCents(cents) };
|
||||
},
|
||||
|
||||
// "2×$50, 1×$25, 2×$1" — the breakdown as a dealer would say it out loud.
|
||||
//
|
||||
// A table with no chip legend gets nothing rather than "$132 no chip covers": it
|
||||
// hasn't told us its denominations, so there is no advice to give and the amount
|
||||
// stands on its own. A legend that exists but can't reach the amount is different,
|
||||
// and does say so.
|
||||
formatChipBreakdown(amount, legend) {
|
||||
const hasDenominations = (legend || []).some(chip => this.toCents(chip.value) > 0);
|
||||
if (!hasDenominations) return "";
|
||||
|
||||
const { chips, remainder } = this.chipBreakdown(amount, legend);
|
||||
if (chips.length === 0 && remainder === 0) return "";
|
||||
const parts = chips.map(chip => `${chip.count}×${this.format(chip.value)}`);
|
||||
if (remainder > 0) parts.push(`${this.format(remainder)} no chip covers`);
|
||||
return parts.join(", ");
|
||||
},
|
||||
|
||||
// Should focusing this field select what's already in it?
|
||||
//
|
||||
// Every amount box in this tool opens pre-filled with a suggestion: the raise pad
|
||||
// offers the minimum, Add chips offers a buy-in, the seat editor offers the stack
|
||||
// it already has. A dealer taps the box because they want a *different* number,
|
||||
// and a tap puts the caret after the suggestion instead of over it. Measured on a
|
||||
// phone: the raise pad showing 4, tap, type "60" — the field reads 460. Add chips
|
||||
// showing 200, tap, type "50" — 20050.
|
||||
//
|
||||
// Only number boxes. A name or a note is a thing you edit, and selecting all of
|
||||
// it on focus would be the same mistake pointing the other way.
|
||||
selectsOnFocus(field) {
|
||||
if (!field || field.tagName !== "INPUT") return false;
|
||||
const type = String(field.type || "").toLowerCase();
|
||||
if (type === "number") return true;
|
||||
return type === "text" && String(field.inputMode || "").toLowerCase() === "decimal";
|
||||
},
|
||||
|
||||
// Would a wheel over this field change what it says?
|
||||
//
|
||||
// True only for a focused `<input type="number">`, which a browser treats as a
|
||||
// spinbutton and lets the wheel drive. Every other way of driving one is deliberate
|
||||
// — the arrow keys, the stepper arrows, typing — and all of them are left alone.
|
||||
// Scrolling is the one nobody means: the dealer is reaching for another panel and
|
||||
// the cursor happens to still be over a field they touched a moment ago.
|
||||
//
|
||||
// A predicate rather than a listener so the rule can be tested without a browser,
|
||||
// the same way selectsOnFocus is. main.js does the blurring.
|
||||
wheelWouldEdit(field, activeElement) {
|
||||
// The tag check is belt and braces and known to be so: nothing but an <input>
|
||||
// carries type="number", so the line below already excludes every textarea,
|
||||
// select and button on the page. Deleting it passes every test in this file,
|
||||
// which was checked rather than assumed. It stays for symmetry with
|
||||
// selectsOnFocus above, not because it is load-bearing.
|
||||
if (!field || field.tagName !== "INPUT") return false;
|
||||
if (String(field.type || "").toLowerCase() !== "number") return false;
|
||||
// An unfocused spinbutton ignores the wheel, so there is nothing to defend and
|
||||
// blurring something that isn't focused would be a no-op with a side effect.
|
||||
return activeElement === field;
|
||||
},
|
||||
|
||||
// "+$40.00" / "-$40.00" rather than "$-40.00" — a standings column is read at a
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// A "contribution entry" is { id, remaining, folded }. `remaining` starts as the amount
|
||||
// that player put in the pot and is whittled down round by round as money gets paid out.
|
||||
const PotCalculator = {
|
||||
EPSILON: 0.004,
|
||||
EPSILON: Money.EPSILON,
|
||||
|
||||
// Builds the initial entry list from raw contributions, keeping folded players in
|
||||
// (their chips are still "dead money" owed to whoever wins) while excluding anyone
|
||||
@@ -24,6 +24,101 @@ const PotCalculator = {
|
||||
return entries.filter(e => !e.folded && e.remaining > this.EPSILON);
|
||||
},
|
||||
|
||||
// Splits the money on the table into the main pot and any side pots, with who can
|
||||
// win each. Read-only — this is what a dealer says out loud before the showdown
|
||||
// ("main pot four-eighty, side pot ninety between seats 3 and 5"), which is the
|
||||
// announcement that goes missing far more often than the arithmetic goes wrong.
|
||||
//
|
||||
// Every distinct all-in amount opens a new layer: each player pays into a layer up
|
||||
// to their own contribution, and only players who reached that height can win it.
|
||||
// Folded money stays in — it's dead money owed to whoever wins the layer it fell in.
|
||||
layers(players) {
|
||||
// Uncalled chips were never played for, so they come off the top before any
|
||||
// pot is built. Deriving the pots from what's left is what keeps this function
|
||||
// and uncalledReturns() from disagreeing about the same money — they used to
|
||||
// decide "uncontested" separately, and an overbet that its own bettor was
|
||||
// still live for got announced as a side pot only they could win.
|
||||
const returned = new Map(this.uncalledReturns(players).map(r => [r.id, Money.toCents(r.amount)]));
|
||||
const live = players
|
||||
.map(p => ({ ...p, cents: Money.toCents(p.contribution) - (returned.get(p.id) || 0) }))
|
||||
.filter(p => p.cents > 0);
|
||||
if (live.length === 0) return [];
|
||||
|
||||
const heights = [...new Set(live.map(p => p.cents))].sort((a, b) => a - b);
|
||||
|
||||
const layers = [];
|
||||
let floor = 0;
|
||||
heights.forEach(height => {
|
||||
const band = height - floor;
|
||||
const payers = live.filter(p => p.cents >= height);
|
||||
const amount = band * payers.length;
|
||||
const eligible = payers.filter(p => !p.folded).map(p => p.id);
|
||||
|
||||
if (amount > 0) {
|
||||
if (eligible.length > 0) {
|
||||
layers.push({ amount: Money.fromCents(amount), eligible });
|
||||
} else if (layers.length > 0) {
|
||||
// Everyone who reached this height folded, so it isn't a pot of its
|
||||
// own — it's dead money owed to whoever wins the layer underneath.
|
||||
// Dropping it would quietly shrink the announced pot.
|
||||
const below = layers[layers.length - 1];
|
||||
below.amount = Money.round(below.amount + Money.fromCents(amount));
|
||||
}
|
||||
// With no layer underneath either, nobody in the hand is live at all.
|
||||
// That's a fold flag the dealer needs to fix, not a payout to guess at.
|
||||
}
|
||||
floor = height;
|
||||
});
|
||||
|
||||
// Consecutive layers with the same set of eligible players are one pot to a
|
||||
// dealer — nobody announces two side pots between the same two people.
|
||||
const merged = [];
|
||||
layers.forEach(layer => {
|
||||
const previous = merged[merged.length - 1];
|
||||
const sameEligibility =
|
||||
previous &&
|
||||
previous.eligible.length === layer.eligible.length &&
|
||||
previous.eligible.every(id => layer.eligible.includes(id));
|
||||
|
||||
if (sameEligibility) {
|
||||
previous.amount = Money.round(previous.amount + layer.amount);
|
||||
} else {
|
||||
merged.push({ ...layer });
|
||||
}
|
||||
});
|
||||
|
||||
return merged.map((layer, index) => ({
|
||||
...layer,
|
||||
label: index === 0 ? "Main pot" : `Side pot ${index}`,
|
||||
}));
|
||||
},
|
||||
|
||||
// Chips nobody could call, which the dealer pushes back rather than into the pot.
|
||||
//
|
||||
// A bet is only called up to what somebody *else* was willing to put in, so the
|
||||
// ceiling for each player is the largest contribution among everyone but them.
|
||||
// Measuring against the whole table instead — including the bettor's own chips —
|
||||
// makes the biggest bet its own yardstick, so the one case this exists for (you
|
||||
// shove, a short stack calls for less) comes back empty.
|
||||
uncalledReturns(players) {
|
||||
const contributing = players.filter(p => p.contribution > this.EPSILON);
|
||||
|
||||
// Nobody live at all means the fold flags are wrong, not that every chip on
|
||||
// the table was uncalled. That's a correction for the dealer to make, so say
|
||||
// nothing here and let the wizard ask them to fix it.
|
||||
if (contributing.filter(p => !p.folded).length === 0) return [];
|
||||
|
||||
const cents = contributing.map(p => Money.toCents(p.contribution));
|
||||
|
||||
return contributing
|
||||
.map((p, index) => {
|
||||
const others = cents.filter((_, i) => i !== index);
|
||||
const cap = others.length > 0 ? Math.max(...others) : 0;
|
||||
return { id: p.id, amount: Money.fromCents(cents[index] - cap) };
|
||||
})
|
||||
.filter(r => r.amount > this.EPSILON);
|
||||
},
|
||||
|
||||
// Awards the current pot layer to `winnerIds` (one id, or several for a split/tie).
|
||||
// The payout is capped at the smallest remaining contribution among the winners —
|
||||
// that's the classic side-pot rule: an all-in player can only win what everyone at
|
||||
@@ -40,12 +135,12 @@ const PotCalculator = {
|
||||
if (e.remaining <= this.EPSILON) return e;
|
||||
const used = Math.min(e.remaining, cap);
|
||||
payoutPool += used;
|
||||
return { ...e, remaining: Math.round((e.remaining - used) * 100) / 100 };
|
||||
return { ...e, remaining: Money.round(e.remaining - used) };
|
||||
});
|
||||
|
||||
payoutPool = Math.round(payoutPool * 100) / 100;
|
||||
const baseShareCents = Math.floor((payoutPool * 100) / winners.length);
|
||||
let leftoverCents = Math.round(payoutPool * 100) - baseShareCents * winners.length;
|
||||
payoutPool = Money.round(payoutPool);
|
||||
const baseShareCents = Math.floor(Money.toCents(payoutPool) / winners.length);
|
||||
let leftoverCents = Money.toCents(payoutPool) - baseShareCents * winners.length;
|
||||
|
||||
const perWinner = winnerIds
|
||||
.filter(id => winners.some(w => w.id === id))
|
||||
@@ -60,7 +155,7 @@ const PotCalculator = {
|
||||
i++;
|
||||
}
|
||||
|
||||
const result = perWinner.map(w => ({ id: w.id, amount: w.amountCents / 100 }));
|
||||
const result = perWinner.map(w => ({ id: w.id, amount: Money.fromCents(w.amountCents) }));
|
||||
const remainingContenders = this.getContenders(updatedEntries);
|
||||
|
||||
return {
|
||||
|
||||
@@ -18,13 +18,28 @@ const PotWizard = {
|
||||
this.playerLabelById = new Map(
|
||||
players.map(p => [p.id, this.labelFor(p, state.seats.indexOf(p))])
|
||||
);
|
||||
this.entries = PotCalculator.buildEntries(
|
||||
players.map(p => ({
|
||||
id: p.id,
|
||||
contribution: HoldemState.computeContribution(p, state.chipLegend, state.basePotAmount),
|
||||
folded: p.folded,
|
||||
}))
|
||||
this.seatIndexById = new Map(players.map(p => [p.id, state.seats.indexOf(p)]));
|
||||
// The label carries name and cards together for the log and the payout lines;
|
||||
// the choice rows want them separately so each can be styled and read on its own.
|
||||
this.nameById = new Map(
|
||||
players.map(p => [p.id, HoldemState.displayName(p, state.seats.indexOf(p))])
|
||||
);
|
||||
this.cardsById = new Map(
|
||||
players.map(p => [p.id, (p.cards || []).filter(Boolean)])
|
||||
);
|
||||
const contributions = players.map(p => ({
|
||||
id: p.id,
|
||||
contribution: HoldemState.computeContribution(p, state.chipLegend, state.basePotAmount),
|
||||
// A seat switched off mid-hand still has its chips in the pot, but it
|
||||
// can no more win them than a folded one can — same dead money, and the
|
||||
// wizard must not offer it as a winner.
|
||||
folded: p.folded || HoldemState.isDeadMoney(p),
|
||||
}));
|
||||
this.entries = PotCalculator.buildEntries(contributions);
|
||||
// Kept so the wizard can show the whole breakdown up front, not just the layer
|
||||
// it happens to be asking about.
|
||||
this.layers = PotCalculator.layers(contributions);
|
||||
this.returns = PotCalculator.uncalledReturns(contributions);
|
||||
this.log = [];
|
||||
this.totals = new Map();
|
||||
|
||||
@@ -38,7 +53,7 @@ const PotWizard = {
|
||||
|
||||
labelFor(player, seatIndex) {
|
||||
const name = HoldemState.displayName(player, seatIndex);
|
||||
const cards = (player.cards || []).filter(Boolean).join(" ");
|
||||
const cards = (player.cards || []).filter(Boolean).map(c => Render.formatCard(c)).join(" ");
|
||||
return cards ? `${name} (${cards})` : name;
|
||||
},
|
||||
|
||||
@@ -46,7 +61,54 @@ const PotWizard = {
|
||||
this.totals.set(id, (this.totals.get(id) || 0) + amount);
|
||||
},
|
||||
|
||||
// A chopped pot rarely divides exactly, and the odd chip goes to the first player
|
||||
// left of the button — not to whoever the dealer happened to tick first. The
|
||||
// calculator hands leftovers out in the order it's given the winners, so ordering
|
||||
// them here is what makes the payout match the rule.
|
||||
orderFromButton(ids) {
|
||||
const distance = new Map();
|
||||
for (let step = 1; step <= this.state.seatCount; step++) {
|
||||
distance.set(HoldemState.nextSeatIndex(this.state, this.state.dealerSeatIndex, step), step);
|
||||
}
|
||||
return [...ids].sort((a, b) =>
|
||||
(distance.get(this.seatIndexById.get(a)) ?? 0) - (distance.get(this.seatIndexById.get(b)) ?? 0)
|
||||
);
|
||||
},
|
||||
|
||||
renderStep() {
|
||||
this.renderStepInto();
|
||||
this.markOverflow();
|
||||
},
|
||||
|
||||
// Say when the list runs on past the bottom of the dialog.
|
||||
//
|
||||
// The body scrolls and the footer stays put, which is the right shape and was as far
|
||||
// as it went: nothing on screen said there was anything below. Measured on a Galaxy
|
||||
// S8, a three-way pot showed two names and cut the third off on a row boundary, just
|
||||
// under a hairline that reads as the end of the list. A dealer would have ticked the
|
||||
// two they could see. On the dialog that pays the table, a list that silently
|
||||
// continues is not a small thing.
|
||||
markOverflow() {
|
||||
const body = document.getElementById("potWizardBody");
|
||||
const modal = document.getElementById("potWizardModal");
|
||||
if (!body || !modal) return;
|
||||
|
||||
const update = () => {
|
||||
const below = body.scrollHeight - body.clientHeight - body.scrollTop;
|
||||
modal.classList.toggle("potWizardMoreBelow", below > 1);
|
||||
};
|
||||
if (!this.watchingOverflow) {
|
||||
body.addEventListener("scroll", update, { passive: true });
|
||||
window.addEventListener("resize", update);
|
||||
this.watchingOverflow = true;
|
||||
}
|
||||
update();
|
||||
// Again once the browser has laid the step out — on the first open the body has
|
||||
// no height yet, and an answer taken then is always "nothing below".
|
||||
requestAnimationFrame(update);
|
||||
},
|
||||
|
||||
renderStepInto() {
|
||||
// Auto-resolve any round where only one player is left standing — nothing to ask.
|
||||
let contenders = PotCalculator.getContenders(this.entries);
|
||||
while (contenders.length === 1) {
|
||||
@@ -58,6 +120,12 @@ const PotWizard = {
|
||||
|
||||
const body = document.getElementById("potWizardBody");
|
||||
body.innerHTML = "";
|
||||
// The footer is a sibling of the body, outside the scroll — see the note in
|
||||
// TexasHoldem.cshtml. Cleared alongside it so a step can never inherit the
|
||||
// previous step's buttons.
|
||||
const footer = this.footer();
|
||||
footer.innerHTML = "";
|
||||
if (this.log.length === 0) body.appendChild(this.renderBreakdown());
|
||||
body.appendChild(this.renderLog());
|
||||
|
||||
const remaining = PotCalculator.totalPot(this.entries.filter(e => e.remaining > PotCalculator.EPSILON));
|
||||
@@ -65,7 +133,17 @@ const PotWizard = {
|
||||
if (contenders.length === 0 && remaining > PotCalculator.EPSILON) {
|
||||
const warning = document.createElement("p");
|
||||
warning.className = "potWizardPrompt";
|
||||
warning.textContent = `${Money.format(remaining)} is still unassigned, but every remaining player is marked Folded. Uncheck Fold on whoever should win it.`;
|
||||
|
||||
// Money left over because nobody could call it isn't a pot anyone won —
|
||||
// it goes back to whoever put it in. Telling the dealer to un-fold someone
|
||||
// to "win" it would be wrong.
|
||||
const returned = (this.returns || []).reduce((sum, r) => sum + r.amount, 0);
|
||||
if (Math.abs(returned - remaining) <= PotCalculator.EPSILON && returned > 0) {
|
||||
const who = this.returns.map(r => this.playerLabelById.get(r.id)).join(", ");
|
||||
warning.textContent = `${Money.format(remaining)} was never called — push it back to ${who} rather than paying it out.`;
|
||||
} else {
|
||||
warning.textContent = `${Money.format(remaining)} is still unassigned, but every remaining player is marked Folded. Uncheck Fold on whoever should win it.`;
|
||||
}
|
||||
body.appendChild(warning);
|
||||
body.appendChild(this.renderSummary());
|
||||
return;
|
||||
@@ -79,25 +157,88 @@ const PotWizard = {
|
||||
const round = document.createElement("div");
|
||||
round.className = "potWizardRound";
|
||||
|
||||
const showdown = this.readShowdown(contenders);
|
||||
const decided = showdown && showdown.complete ? new Set(showdown.winners) : null;
|
||||
const handById = new Map((showdown ? showdown.players : []).map(p => [p.id, p.hand]));
|
||||
|
||||
const prompt = document.createElement("p");
|
||||
prompt.className = "potWizardPrompt";
|
||||
prompt.innerHTML = this.log.length === 0
|
||||
? `Who won the hand? <strong>${Money.format(remaining)}</strong> is in play.`
|
||||
: `That side pot still leaves <strong>${Money.format(remaining)}</strong> unassigned — who's next best?`;
|
||||
prompt.innerHTML = decided
|
||||
// It has read the hands, so it stops asking a question it knows the answer
|
||||
// to. The list stays — the dealer confirms, and a hand entered wrong is
|
||||
// corrected by unticking rather than by starting over.
|
||||
? `<strong>${Money.format(remaining)}</strong> in play.`
|
||||
: this.log.length === 0
|
||||
? `Who won the hand? <strong>${Money.format(remaining)}</strong> is in play.`
|
||||
: `That side pot still leaves <strong>${Money.format(remaining)}</strong> unassigned — who's next best?`;
|
||||
round.appendChild(prompt);
|
||||
|
||||
const verdict = this.renderVerdict(showdown, contenders);
|
||||
if (verdict) round.appendChild(verdict);
|
||||
|
||||
const choices = document.createElement("div");
|
||||
choices.className = "potWizardChoices";
|
||||
contenders.forEach(c => {
|
||||
const label = document.createElement("label");
|
||||
label.className = "potWizardChoice";
|
||||
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.value = c.id;
|
||||
const text = document.createElement("span");
|
||||
text.textContent = `${this.playerLabelById.get(c.id)} — up to ${Money.format(c.remaining)}`;
|
||||
if (decided && decided.has(c.id)) {
|
||||
checkbox.checked = true;
|
||||
label.classList.add("chosen");
|
||||
}
|
||||
// The row is the target, not the box — so the row has to look chosen too.
|
||||
checkbox.addEventListener("change", () => {
|
||||
label.classList.toggle("chosen", checkbox.checked);
|
||||
});
|
||||
label.appendChild(checkbox);
|
||||
|
||||
const text = document.createElement("span");
|
||||
text.className = "potWizardChoiceText";
|
||||
|
||||
const name = document.createElement("span");
|
||||
name.className = "potWizardChoiceName";
|
||||
name.textContent = this.nameById.get(c.id);
|
||||
text.appendChild(name);
|
||||
|
||||
// How this hand was read, under the name — on every row rather than only
|
||||
// the winning one, because a dealer checking the tool's answer is reading
|
||||
// the losers' hands, and a row that says nothing is a row they have to work
|
||||
// out for themselves. It takes the line the ceiling used to have; the
|
||||
// ceiling moves out beside the name, which is where the phone layout had
|
||||
// already put it to save exactly this line.
|
||||
const read = handById.get(c.id);
|
||||
if (read) {
|
||||
const ranked = document.createElement("span");
|
||||
ranked.className = "potWizardChoiceHand";
|
||||
ranked.textContent = HandRank.describe(read);
|
||||
text.appendChild(ranked);
|
||||
}
|
||||
label.appendChild(text);
|
||||
|
||||
const upTo = document.createElement("span");
|
||||
upTo.className = "potWizardChoiceUpTo";
|
||||
upTo.textContent = `up to ${Money.format(c.remaining)}`;
|
||||
label.appendChild(upTo);
|
||||
|
||||
// Cards sit apart from the name rather than inside it, so a dealer reading
|
||||
// a row across the table sees the hand, not a sentence.
|
||||
const cards = this.cardsById.get(c.id) || [];
|
||||
if (cards.length > 0) {
|
||||
const cardsEl = document.createElement("span");
|
||||
cardsEl.className = "potWizardChoiceCards";
|
||||
cards.forEach((card, i) => {
|
||||
if (i > 0) cardsEl.appendChild(document.createTextNode(" "));
|
||||
const span = document.createElement("span");
|
||||
if (/[HD]$/i.test(card)) span.className = "cardRed";
|
||||
span.textContent = Render.formatCard(card);
|
||||
cardsEl.appendChild(span);
|
||||
});
|
||||
label.appendChild(cardsEl);
|
||||
}
|
||||
|
||||
choices.appendChild(label);
|
||||
});
|
||||
round.appendChild(choices);
|
||||
@@ -109,18 +250,104 @@ const PotWizard = {
|
||||
|
||||
const confirmBtn = document.createElement("button");
|
||||
confirmBtn.className = "holdemBtn holdemBtnAccent";
|
||||
confirmBtn.textContent = "Confirm Winner(s)";
|
||||
confirmBtn.textContent = decided
|
||||
? (showdown.winners.length === 1
|
||||
? `Pay ${this.nameById.get(showdown.winners[0])}`
|
||||
: "Split the pot")
|
||||
: "Confirm Winner(s)";
|
||||
confirmBtn.addEventListener("click", () => {
|
||||
const selected = Array.from(choices.querySelectorAll("input:checked")).map(i => i.value);
|
||||
if (selected.length === 0) return;
|
||||
const result = PotCalculator.resolveRound(this.entries, selected);
|
||||
const result = PotCalculator.resolveRound(this.entries, this.orderFromButton(selected));
|
||||
this.recordRound(result);
|
||||
this.entries = result.updatedEntries;
|
||||
this.renderStep();
|
||||
});
|
||||
round.appendChild(confirmBtn);
|
||||
|
||||
body.appendChild(round);
|
||||
footer.appendChild(confirmBtn);
|
||||
},
|
||||
|
||||
// How the tool reads the hands still in for this round of the pot.
|
||||
//
|
||||
// Asked per round rather than once for the whole hand, because a side pot has its
|
||||
// own contenders: the player who won the main pot is out of the question for the
|
||||
// next one, and the best hand among who is left is a different answer.
|
||||
//
|
||||
// Only when the dealer asked to track the board — everything here is off by default
|
||||
// and the wizard behaves exactly as it always has.
|
||||
readShowdown(contenders) {
|
||||
if (!this.state.trackBoard || typeof HandRank === "undefined") return null;
|
||||
return HandRank.showdown(this.state.board, contenders.map(c => ({
|
||||
id: c.id,
|
||||
cards: this.cardsById.get(c.id) || [],
|
||||
})));
|
||||
},
|
||||
|
||||
// What the winners would actually be paid if this round were confirmed now.
|
||||
//
|
||||
// Worked out from the same function that does the paying rather than from a second
|
||||
// one that agrees with it today — a preview that can drift from the payout is worse
|
||||
// than no preview, because it is the number the dealer says out loud.
|
||||
previewPayout(winnerIds) {
|
||||
try {
|
||||
const result = PotCalculator.resolveRound(this.entries, this.orderFromButton(winnerIds));
|
||||
return result.perWinner || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// "Alexandra wins $25 with a full house, kings full of threes."
|
||||
//
|
||||
// The whole point of the feature, in one line: who won and how much. It goes above
|
||||
// the list rather than replacing it, because the dealer still confirms — the tool
|
||||
// reads the cards, a person still says the pot is good.
|
||||
renderVerdict(showdown, contenders) {
|
||||
if (!showdown) return null;
|
||||
|
||||
const line = document.createElement("p");
|
||||
line.className = "potWizardVerdict";
|
||||
|
||||
if (!showdown.boardComplete) {
|
||||
line.classList.add("potWizardVerdictUnsure");
|
||||
line.textContent = "The board isn't all five cards yet, so pick the winner yourself — or fill it in on the felt and this will read it.";
|
||||
return line;
|
||||
}
|
||||
if (showdown.duplicated) {
|
||||
line.classList.add("potWizardVerdictUnsure");
|
||||
line.textContent = "The same card is in two places, so this can't be read. Check the cards on the felt.";
|
||||
return line;
|
||||
}
|
||||
if (!showdown.complete) {
|
||||
const missing = showdown.unknown.map(id => this.nameById.get(id)).filter(Boolean);
|
||||
line.classList.add("potWizardVerdictUnsure");
|
||||
line.textContent = missing.length
|
||||
? `${this.listOf(missing)} ${missing.length === 1 ? "hasn't" : "haven't"} turned over, so who won is still yours to say.`
|
||||
: "Not enough cards to read this one.";
|
||||
return line;
|
||||
}
|
||||
|
||||
const paid = this.previewPayout(showdown.winners);
|
||||
const amount = paid.length ? Money.format(paid[0].amount) : "";
|
||||
const names = showdown.winners.map(id => this.nameById.get(id)).filter(Boolean);
|
||||
const hand = showdown.players.find(p => p.id === showdown.winners[0]);
|
||||
const described = HandRank.phrase(hand && hand.hand);
|
||||
|
||||
line.classList.add("potWizardVerdictSure");
|
||||
line.textContent = names.length === 1
|
||||
? `${names[0]} wins ${amount} with ${described}.`
|
||||
: `${this.listOf(names)} split it, ${amount} each, with ${described}.`;
|
||||
return line;
|
||||
},
|
||||
|
||||
listOf(names) {
|
||||
if (names.length <= 1) return names[0] || "";
|
||||
return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
||||
},
|
||||
|
||||
// The row of buttons that never scrolls away.
|
||||
footer() {
|
||||
return document.getElementById("potWizardFooter");
|
||||
},
|
||||
|
||||
recordRound(result) {
|
||||
@@ -131,6 +358,20 @@ const PotWizard = {
|
||||
});
|
||||
},
|
||||
|
||||
// The announcement, spelled out before anyone is asked to pick a winner: what the
|
||||
// main pot is worth, what each side pot is worth, and who can win each.
|
||||
renderBreakdown() {
|
||||
const node = Render.buildPotBreakdown(
|
||||
this.layers || [],
|
||||
this.returns || [],
|
||||
id => this.playerLabelById.get(id),
|
||||
"Announce before the showdown"
|
||||
);
|
||||
if (!node) return document.createDocumentFragment();
|
||||
node.classList.add("potBreakdownWizard");
|
||||
return node;
|
||||
},
|
||||
|
||||
renderLog() {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "potWizardLog";
|
||||
@@ -145,6 +386,24 @@ const PotWizard = {
|
||||
return wrap;
|
||||
},
|
||||
|
||||
// What the house takes out of this pot, and what is left to push.
|
||||
//
|
||||
// Worked out here rather than at the end so the dealer sees it before anything
|
||||
// moves: the summary is the last chance to disagree with the arithmetic, and a
|
||||
// rake that only appeared in the hand log afterwards would be the tool taking
|
||||
// money quietly.
|
||||
rakeSplit() {
|
||||
const gross = Array.from(this.totals.entries()).map(([id, amount]) => ({
|
||||
id,
|
||||
label: this.playerLabelById.get(id),
|
||||
amount,
|
||||
}));
|
||||
const pot = gross.reduce((sum, p) => sum + p.amount, 0);
|
||||
const sawFlop = (this.state.street || "preflop") !== "preflop";
|
||||
const rake = HoldemState.rakeOn(this.state, pot, sawFlop);
|
||||
return { pot, sawFlop, ...HoldemState.applyRake(gross, rake) };
|
||||
},
|
||||
|
||||
renderSummary() {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "potWizardSummary";
|
||||
@@ -153,16 +412,43 @@ const PotWizard = {
|
||||
heading.textContent = "Final Payouts";
|
||||
wrap.appendChild(heading);
|
||||
|
||||
const split = this.rakeSplit();
|
||||
const rakeSet = Number((this.state.rake || {}).percent) > 0;
|
||||
if (rakeSet && this.totals.size > 0) {
|
||||
const line = document.createElement("p");
|
||||
line.className = "potWizardRake";
|
||||
const cap = Number(this.state.rake.cap) > 0 ? `, capped at ${Money.format(this.state.rake.cap)}` : "";
|
||||
line.textContent = split.rake > 0
|
||||
? `House takes ${Money.format(split.rake)} of ${Money.format(split.pot)} — ${this.state.rake.percent}%${cap}.`
|
||||
: split.sawFlop
|
||||
? `No rake on this one — ${this.state.rake.percent}% of ${Money.format(split.pot)} doesn't make a chip the table has.`
|
||||
: "No flop, no drop — nothing taken from a pot that ended before the flop.";
|
||||
wrap.appendChild(line);
|
||||
}
|
||||
|
||||
if (this.totals.size === 0) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "fieldHint";
|
||||
empty.textContent = "Nobody has money in this pot yet — mark seats \"In hand\" and set contributions first.";
|
||||
wrap.appendChild(empty);
|
||||
} else {
|
||||
const legend = (this.state && this.state.chipLegend) || [];
|
||||
const list = document.createElement("ul");
|
||||
for (const [id, amount] of this.totals.entries()) {
|
||||
for (const { id, amount } of split.payouts) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = `${this.playerLabelById.get(id)} — ${Money.format(amount)}`;
|
||||
|
||||
// The amount is the answer to the arithmetic; the chips are the answer
|
||||
// to the dealer's job. Only when the legend has been filled in — a
|
||||
// table that never set up chip colours gets the number on its own,
|
||||
// which is what it had before.
|
||||
const chips = Money.formatChipBreakdown(amount, legend);
|
||||
if (chips) {
|
||||
const note = document.createElement("span");
|
||||
note.className = "payoutChips";
|
||||
note.textContent = chips;
|
||||
li.appendChild(note);
|
||||
}
|
||||
list.appendChild(li);
|
||||
}
|
||||
wrap.appendChild(list);
|
||||
@@ -175,23 +461,34 @@ const PotWizard = {
|
||||
applyBtn.className = "holdemBtn holdemBtnAccent";
|
||||
applyBtn.textContent = "Start Next Hand";
|
||||
applyBtn.addEventListener("click", () => {
|
||||
const payouts = Array.from(this.totals.entries()).map(([id, amount]) => ({
|
||||
id,
|
||||
label: this.playerLabelById.get(id),
|
||||
amount,
|
||||
}));
|
||||
// Recomputed rather than reused from the render: the same numbers, but
|
||||
// taken at the moment the dealer commits rather than when they last
|
||||
// looked at the screen.
|
||||
const paid = this.rakeSplit();
|
||||
this.close();
|
||||
this.onFinished(payouts);
|
||||
this.onFinished(paid.payouts, { rake: paid.rake, pot: paid.pot });
|
||||
});
|
||||
actions.appendChild(applyBtn);
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.className = "holdemBtn holdemBtnGhost";
|
||||
closeBtn.textContent = "Close";
|
||||
closeBtn.addEventListener("click", () => this.close());
|
||||
actions.appendChild(closeBtn);
|
||||
// This button cancels — it does not pay anybody. Under a heading reading
|
||||
// "Final Payouts" with an amount beside a name, "Close" reads like dismissing
|
||||
// something already done, and a dealer who taps it walks away believing the
|
||||
// pot has been pushed. It hasn't: the chips are still in the middle and the
|
||||
// hand isn't logged. So it says what it does.
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.className = "holdemBtn holdemBtnGhost";
|
||||
cancelBtn.textContent = "Cancel — don't pay yet";
|
||||
cancelBtn.addEventListener("click", () => this.close());
|
||||
actions.appendChild(cancelBtn);
|
||||
|
||||
wrap.appendChild(actions);
|
||||
if (this.totals.size > 0) {
|
||||
const note = document.createElement("p");
|
||||
note.className = "fieldHint potWizardPendingNote";
|
||||
note.textContent = "Nothing has been paid out yet. Start Next Hand pushes the pot and logs the hand.";
|
||||
wrap.appendChild(note);
|
||||
}
|
||||
|
||||
this.footer().appendChild(actions);
|
||||
return wrap;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,12 +15,28 @@ const QuickSplit = {
|
||||
ways: 2,
|
||||
chip: 1,
|
||||
|
||||
// "even" is the common case — one pot, everyone matched. "uneven" is the one this
|
||||
// screen used to send people back to the full table for: somebody all-in for less,
|
||||
// so the money splits into a main pot and side pots.
|
||||
mode: "even",
|
||||
entries: [],
|
||||
|
||||
mount() {
|
||||
this.potInput = document.getElementById("potInput");
|
||||
this.clearPotBtn = document.getElementById("clearPotBtn");
|
||||
this.entries = [{ amount: "", folded: false }, { amount: "", folded: false }, { amount: "", folded: false }];
|
||||
|
||||
this.renderWayChoices();
|
||||
this.renderChipChoices();
|
||||
this.renderUnevenRows();
|
||||
|
||||
document.getElementById("modeEvenBtn").addEventListener("click", () => this.setMode("even"));
|
||||
document.getElementById("modeUnevenBtn").addEventListener("click", () => this.setMode("uneven"));
|
||||
document.getElementById("addPlayerBtn").addEventListener("click", () => {
|
||||
this.entries.push({ amount: "", folded: false });
|
||||
this.renderUnevenRows();
|
||||
this.update();
|
||||
});
|
||||
|
||||
this.potInput.addEventListener("input", () => {
|
||||
this.sanitizePotInput();
|
||||
@@ -127,7 +143,196 @@ const QuickSplit = {
|
||||
});
|
||||
},
|
||||
|
||||
setMode(mode) {
|
||||
this.mode = mode;
|
||||
document.getElementById("modeEvenBtn").classList.toggle("selected", mode === "even");
|
||||
document.getElementById("modeUnevenBtn").classList.toggle("selected", mode === "uneven");
|
||||
|
||||
document.querySelectorAll(".evenOnly").forEach(el => { el.hidden = mode !== "even"; });
|
||||
document.querySelectorAll(".unevenOnly").forEach(el => { el.hidden = mode !== "uneven"; });
|
||||
document.getElementById("splitResult").hidden = mode !== "even";
|
||||
document.getElementById("unevenResult").hidden = mode !== "uneven";
|
||||
|
||||
this.update();
|
||||
},
|
||||
|
||||
renderUnevenRows() {
|
||||
const host = document.getElementById("unevenRows");
|
||||
const template = document.getElementById("unevenRowTemplate");
|
||||
host.innerHTML = "";
|
||||
|
||||
this.entries.forEach((entry, index) => {
|
||||
const row = template.content.firstElementChild.cloneNode(true);
|
||||
row.querySelector(".unevenRowLabel").textContent = `P${index + 1}`;
|
||||
|
||||
const amount = row.querySelector(".unevenAmount");
|
||||
amount.value = entry.amount;
|
||||
amount.setAttribute("aria-label", `Player ${index + 1} amount`);
|
||||
amount.addEventListener("input", () => {
|
||||
entry.amount = amount.value.replace(/[^0-9.]/g, "").replace(/(\..*)\./g, "$1");
|
||||
if (entry.amount !== amount.value) amount.value = entry.amount;
|
||||
this.update();
|
||||
});
|
||||
|
||||
// Folded money stays in the pot as dead money, but its owner can't win any
|
||||
// of it — which is exactly the distinction that makes side pots tricky.
|
||||
const fold = row.querySelector(".unevenFoldBtn");
|
||||
fold.classList.toggle("selected", entry.folded);
|
||||
fold.setAttribute("aria-pressed", String(entry.folded));
|
||||
fold.addEventListener("click", () => {
|
||||
entry.folded = !entry.folded;
|
||||
this.renderUnevenRows();
|
||||
this.update();
|
||||
});
|
||||
|
||||
const remove = row.querySelector(".unevenRemoveBtn");
|
||||
remove.disabled = this.entries.length <= 2;
|
||||
remove.addEventListener("click", () => {
|
||||
this.entries.splice(index, 1);
|
||||
this.renderUnevenRows();
|
||||
this.update();
|
||||
});
|
||||
|
||||
host.appendChild(row);
|
||||
});
|
||||
},
|
||||
|
||||
// Chopping one of the pots listed above, without leaving the screen that worked it
|
||||
// out. Starts closed and at as many ways as there are players who can win the pot,
|
||||
// which is the common case; the stepper covers a chop between fewer of them, which
|
||||
// is the other one.
|
||||
//
|
||||
// The per-pot ways live on the row rather than in `this.ways`, so opening a chop on
|
||||
// the main pot doesn't change what the side pot below it is saying.
|
||||
addChopControl(row, amount, contenderCount) {
|
||||
let ways = contenderCount;
|
||||
let open = false;
|
||||
|
||||
const toggle = document.createElement("button");
|
||||
toggle.type = "button";
|
||||
toggle.className = "unevenChopToggle";
|
||||
|
||||
const detail = document.createElement("div");
|
||||
detail.className = "unevenChopDetail";
|
||||
detail.hidden = true;
|
||||
|
||||
const draw = () => {
|
||||
toggle.textContent = open ? "Hide chop" : "Chop";
|
||||
toggle.setAttribute("aria-expanded", String(open));
|
||||
detail.hidden = !open;
|
||||
if (!open) return;
|
||||
|
||||
const result = SplitCalculator.split(amount, ways, this.chip);
|
||||
detail.innerHTML = "";
|
||||
|
||||
const stepper = document.createElement("div");
|
||||
stepper.className = "unevenChopWays";
|
||||
stepper.appendChild(this.makeStepButton("−", () => {
|
||||
if (ways > 2) { ways -= 1; draw(); }
|
||||
}));
|
||||
const label = document.createElement("span");
|
||||
label.className = "unevenChopWaysLabel";
|
||||
label.textContent = `${ways} ways`;
|
||||
stepper.appendChild(label);
|
||||
stepper.appendChild(this.makeStepButton("+", () => {
|
||||
if (ways < Math.max(contenderCount, this.MAX_WAYS)) { ways += 1; draw(); }
|
||||
}));
|
||||
detail.appendChild(stepper);
|
||||
|
||||
const each = document.createElement("p");
|
||||
each.className = "unevenChopEach";
|
||||
each.textContent = `${Money.format(result.each)} each`;
|
||||
detail.appendChild(each);
|
||||
|
||||
// Same wording as the main split, because it is the same rule and a dealer
|
||||
// shouldn't have to notice they're reading a different screen.
|
||||
if (result.valid && result.oddChips > 0) {
|
||||
const odd = document.createElement("p");
|
||||
odd.className = "unevenChopNote";
|
||||
odd.textContent = result.oddChips === 1
|
||||
? `plus one odd ${Money.format(result.chip)} chip to the first player left of the button`
|
||||
: `plus ${result.oddChips} odd ${Money.format(result.chip)} chips — one each to the ` +
|
||||
`first ${result.oddChips} players left of the button`;
|
||||
detail.appendChild(odd);
|
||||
}
|
||||
if (result.valid && result.dust > 0) {
|
||||
const dust = document.createElement("p");
|
||||
dust.className = "unevenChopNote unevenChopDust";
|
||||
dust.textContent =
|
||||
`${Money.format(result.dust)} doesn't divide into ${Money.format(result.chip)} chips — ` +
|
||||
`pick a smaller denomination.`;
|
||||
detail.appendChild(dust);
|
||||
}
|
||||
};
|
||||
|
||||
toggle.addEventListener("click", () => { open = !open; draw(); });
|
||||
draw();
|
||||
row.appendChild(toggle);
|
||||
row.appendChild(detail);
|
||||
},
|
||||
|
||||
updateUneven() {
|
||||
const players = this.entries
|
||||
.map((entry, index) => ({ id: `P${index + 1}`, contribution: Number(entry.amount) || 0, folded: entry.folded }))
|
||||
.filter(p => p.contribution > 0);
|
||||
|
||||
const pots = PotCalculator.layers(players);
|
||||
const returns = PotCalculator.uncalledReturns(players);
|
||||
|
||||
const host = document.getElementById("unevenPots");
|
||||
const empty = document.getElementById("unevenEmpty");
|
||||
host.innerHTML = "";
|
||||
|
||||
if (pots.length === 0 && returns.length === 0) {
|
||||
empty.hidden = false;
|
||||
empty.textContent = players.length === 0
|
||||
? "Enter what each player put in."
|
||||
: "Nobody left who can win — un-fold whoever should take it.";
|
||||
return;
|
||||
}
|
||||
empty.hidden = true;
|
||||
|
||||
const addRow = (label, amount, who, className, chopWays) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = `unevenPot ${className || ""}`;
|
||||
const name = document.createElement("span");
|
||||
name.className = "unevenPotLabel";
|
||||
name.textContent = label;
|
||||
const value = document.createElement("span");
|
||||
value.className = "unevenPotAmount";
|
||||
value.textContent = Money.format(amount);
|
||||
const eligible = document.createElement("span");
|
||||
eligible.className = "unevenPotWho";
|
||||
eligible.textContent = who;
|
||||
row.appendChild(name);
|
||||
row.appendChild(value);
|
||||
row.appendChild(eligible);
|
||||
host.appendChild(row);
|
||||
|
||||
// Two players tying for a side pot is ordinary, and working out each one's
|
||||
// share meant copying the amount into the other mode by hand — retyping a
|
||||
// number the screen had just worked out, which is a transcription error
|
||||
// waiting to happen with money on it. The chop happens here instead.
|
||||
//
|
||||
// Only offered where it means something: a pot with at least two people
|
||||
// who can win it. Not the uncalled row, which goes back to one player, and
|
||||
// not the total.
|
||||
if (chopWays >= 2) this.addChopControl(row, amount, chopWays);
|
||||
};
|
||||
|
||||
pots.forEach(pot => addRow(pot.label, pot.amount, pot.eligible.join(", "), "", pot.eligible.length));
|
||||
returns.forEach(ret => addRow("Uncalled", ret.amount, `back to ${ret.id}`, "unevenPotReturn"));
|
||||
|
||||
const total = players.reduce((sum, p) => sum + p.contribution, 0);
|
||||
addRow("On the table", Money.round(total), "", "unevenPotTotal");
|
||||
},
|
||||
|
||||
update() {
|
||||
if (this.mode === "uneven") {
|
||||
this.updateUneven();
|
||||
return;
|
||||
}
|
||||
|
||||
const potValue = Number(this.potInput.value);
|
||||
const pot = Number.isNaN(potValue) ? 0 : potValue;
|
||||
this.clearPotBtn.hidden = this.potInput.value === "";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,178 @@
|
||||
// The long-press menu on a seat. Everything a dealer does to one player mid-hand,
|
||||
// one press and one tap away, without opening the full editor and without hunting
|
||||
// for whose turn it is in the action bar.
|
||||
const SeatMenu = {
|
||||
state: null,
|
||||
hooks: null,
|
||||
seatIndex: null,
|
||||
|
||||
mode: null,
|
||||
|
||||
open(state, hooks, seatIndex) {
|
||||
this.state = state;
|
||||
this.hooks = hooks;
|
||||
this.seatIndex = seatIndex;
|
||||
// Always the action list, never whatever the sheet was showing when it last
|
||||
// closed — a seat menu that opens on a list of chairs is a menu that lost the
|
||||
// dealer's place.
|
||||
this.mode = null;
|
||||
this.render();
|
||||
document.getElementById("seatMenuOverlay").hidden = false;
|
||||
},
|
||||
|
||||
close() {
|
||||
document.getElementById("seatMenuOverlay").hidden = true;
|
||||
},
|
||||
|
||||
// Only the actions that make sense for this seat right now — a folded player has
|
||||
// nothing to go all-in with, and an empty chair has nothing to fold.
|
||||
actionsFor(player, index) {
|
||||
const actions = [];
|
||||
const dealtIn = player.inHand;
|
||||
const live = dealtIn && !player.folded;
|
||||
|
||||
if (live) {
|
||||
actions.push({ label: "Fold", className: "menuDanger", run: () => this.hooks.fold(index) });
|
||||
actions.push({ label: "All-in", className: "menuAccent", run: () => this.hooks.allIn(index) });
|
||||
} else if (dealtIn && player.folded) {
|
||||
actions.push({ label: "Un-fold", run: () => this.hooks.unfold(index) });
|
||||
}
|
||||
|
||||
// The most common interruption in a cash game, and until now it meant opening
|
||||
// the editor and retyping a stack from memory.
|
||||
actions.push({ label: "Add chips…", run: () => this.hooks.addChips(index) });
|
||||
|
||||
actions.push({
|
||||
label: dealtIn ? "Sit out" : "Deal in",
|
||||
run: () => this.hooks.toggleDealtIn(index),
|
||||
});
|
||||
|
||||
// An empty chair with people waiting: the dealer taps the chair somebody is
|
||||
// about to sit in, which is what their hand is doing anyway. Only the front of
|
||||
// the queue is offered, for the same reason the panel only offers them — a list
|
||||
// that isn't kept in order is just a pile of names.
|
||||
const waiting = (this.state.waitingList || [])[0];
|
||||
const emptyChair = !HoldemState.seatInPlay(player);
|
||||
if (waiting && emptyChair) {
|
||||
actions.push({
|
||||
label: `Seat ${waiting.name}`,
|
||||
className: "menuAccent",
|
||||
run: () => this.hooks.seatWaiting(waiting.id, index),
|
||||
});
|
||||
}
|
||||
|
||||
// Leaving for the night, which is a different thing from sitting out a hand:
|
||||
// the chips go back over the rail and the chair opens. Offered only for a seat
|
||||
// that has somebody in it, since an empty chair has nobody to pay.
|
||||
if (HoldemState.seatInPlay(player)) {
|
||||
actions.push({ label: "Cash out…", run: () => this.hooks.cashOut(index) });
|
||||
}
|
||||
|
||||
if (!this.state.dedicatedDealer && index !== this.state.dealerSeatIndex) {
|
||||
actions.push({ label: "Give the button", run: () => this.hooks.makeDealer(index) });
|
||||
}
|
||||
|
||||
// Naming a blind by hand. Only offered while nothing but the blinds are in —
|
||||
// once somebody has called, moving a blind would mean unpicking a bet that
|
||||
// has already been answered, and that's a decision for the dealer's mouth,
|
||||
// not a menu item. Which blind a seat already has is shown rather than
|
||||
// offered again.
|
||||
if (dealtIn && Betting.onlyBlindsIn(this.state)) {
|
||||
const blinds = HoldemState.getBlindSeats(this.state);
|
||||
if (blinds.sbIndex !== index) {
|
||||
actions.push({ label: "Make small blind", run: () => this.hooks.assignBlind(index, "sb") });
|
||||
}
|
||||
if (blinds.bbIndex !== index) {
|
||||
actions.push({ label: "Make big blind", run: () => this.hooks.assignBlind(index, "bb") });
|
||||
}
|
||||
if (this.state.blindOverride) {
|
||||
actions.push({ label: "Blinds follow the button again", run: () => this.hooks.clearBlindOverride() });
|
||||
}
|
||||
}
|
||||
|
||||
// Changing chairs was drag-and-drop only, and HTML5 drag events are never
|
||||
// generated from a touch screen — so on the device this tool is mostly used on,
|
||||
// the "drag to swap seats" the hint advertised did nothing at all. This is the
|
||||
// same move, by the route a thumb can actually take.
|
||||
if (this.state.seats.length > 1) {
|
||||
actions.push({ label: "Move to…", run: () => this.openMovePicker() });
|
||||
}
|
||||
|
||||
actions.push({ label: "Edit seat…", run: () => this.hooks.editSeat(index) });
|
||||
return actions;
|
||||
},
|
||||
|
||||
// The chairs this player could move to. Every other chair: swapping into an empty
|
||||
// one is how a player moves, and swapping with an occupied one is two players
|
||||
// changing places, which is the other half of the same job.
|
||||
destinationsFor(index) {
|
||||
return this.state.seats
|
||||
.map((player, seat) => ({ player, seat }))
|
||||
.filter(entry => entry.seat !== index);
|
||||
},
|
||||
|
||||
openMovePicker() {
|
||||
this.mode = "move";
|
||||
this.render();
|
||||
},
|
||||
|
||||
render() {
|
||||
const player = this.state.seats[this.seatIndex];
|
||||
const title = document.getElementById("seatMenuTitle");
|
||||
const subtitle = document.getElementById("seatMenuSubtitle");
|
||||
const list = document.getElementById("seatMenuActions");
|
||||
list.innerHTML = "";
|
||||
|
||||
if (this.mode === "move") {
|
||||
title.textContent = `Move ${HoldemState.displayName(player, this.seatIndex)} to…`;
|
||||
subtitle.textContent = "Their chips, cards and bet go with them. The button and the blinds stay on the chair.";
|
||||
this.destinationsFor(this.seatIndex).forEach(({ player: other, seat }) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "seatMenuBtn";
|
||||
// displayName falls back to "Seat 5" for a chair with nobody in it,
|
||||
// which reads as "Seat 5 — Seat 5". An empty chair says it's empty.
|
||||
const named = (other.name || "").trim();
|
||||
btn.textContent = named ? `Seat ${seat + 1} — ${named}` : `Seat ${seat + 1} — empty`;
|
||||
btn.addEventListener("click", () => {
|
||||
const from = this.seatIndex;
|
||||
this.close();
|
||||
this.hooks.swapSeats(from, seat);
|
||||
});
|
||||
list.appendChild(btn);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
title.textContent = HoldemState.displayName(player, this.seatIndex);
|
||||
|
||||
const stack = Betting.stackOf(this.state, player);
|
||||
const owed = Betting.toCall(this.state, player);
|
||||
const bits = [];
|
||||
if (stack > 0) bits.push(`${Money.format(stack)} behind`);
|
||||
if (owed > 0) bits.push(`${Money.format(owed)} to call`);
|
||||
subtitle.textContent = bits.join(" · ");
|
||||
|
||||
this.actionsFor(player, this.seatIndex).forEach(action => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = `seatMenuBtn ${action.className || ""}`;
|
||||
btn.textContent = action.label;
|
||||
btn.addEventListener("click", () => {
|
||||
// "Move to…" stays on the sheet and swaps it for the seat list; every
|
||||
// other action is the end of the interaction.
|
||||
if (action.label === "Move to…") {
|
||||
action.run();
|
||||
return;
|
||||
}
|
||||
this.close();
|
||||
action.run();
|
||||
});
|
||||
list.appendChild(btn);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = SeatMenu;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Who hands cash to whom at the end of the night.
|
||||
//
|
||||
// The standings panel already works out what everyone is up or down. It then stopped,
|
||||
// which leaves the dealer with six signed numbers at 2am and the actual job still to
|
||||
// do: turning them into payments. That's the step people get wrong — not the poker.
|
||||
//
|
||||
// Everything is done in cents. A settlement that has to come out exactly right is the
|
||||
// last place to let a float land on 41.99999999.
|
||||
const SettleUp = {
|
||||
// Largest debt against largest win, repeatedly. It's greedy rather than optimal,
|
||||
// and for this problem that's the right trade: finding the true minimum number of
|
||||
// transfers is NP-hard, while this is guaranteed to need at most one payment fewer
|
||||
// than there are people, and each payment it does emit clears somebody out of the
|
||||
// list entirely. A dealer wants a short list they can read down, not a proof.
|
||||
//
|
||||
// Payers are the outer loop so that everything one person owes is printed together
|
||||
// — the list is read out loud, one player at a time.
|
||||
plan(standings) {
|
||||
const owed = [];
|
||||
const owing = [];
|
||||
|
||||
(standings || []).forEach(row => {
|
||||
const cents = Money.toCents(row.cashPosition);
|
||||
if (cents > 0) owed.push({ name: row.name, cents });
|
||||
else if (cents < 0) owing.push({ name: row.name, cents: -cents });
|
||||
});
|
||||
|
||||
owed.sort((a, b) => b.cents - a.cents || a.name.localeCompare(b.name));
|
||||
owing.sort((a, b) => b.cents - a.cents || a.name.localeCompare(b.name));
|
||||
|
||||
const payments = [];
|
||||
let creditor = 0;
|
||||
owing.forEach(payer => {
|
||||
while (payer.cents > 0 && creditor < owed.length) {
|
||||
const taker = owed[creditor];
|
||||
const amount = Math.min(payer.cents, taker.cents);
|
||||
payments.push({ from: payer.name, to: taker.name, amount: Money.fromCents(amount) });
|
||||
payer.cents -= amount;
|
||||
taker.cents -= amount;
|
||||
if (taker.cents === 0) creditor++;
|
||||
}
|
||||
});
|
||||
|
||||
// What's left over when the chips on the table don't match the cash that came
|
||||
// in. It is never a rounding artifact — the arithmetic is integer cents — so it
|
||||
// is always the thing the bank check is already warning about: a rebuy nobody
|
||||
// wrote down, or a miscount. Reported rather than quietly spread across the
|
||||
// payments, because inventing a transfer to make the columns agree is how a
|
||||
// real person ends up short.
|
||||
const unpaid = Money.fromCents(owed.reduce((sum, row) => sum + row.cents, 0));
|
||||
const unspent = Money.fromCents(owing.reduce((sum, row) => sum + row.cents, 0));
|
||||
|
||||
return {
|
||||
payments,
|
||||
// Only one of these can be non-zero: either there are more chips than cash
|
||||
// or less, never both.
|
||||
shortBy: unpaid,
|
||||
overBy: unspent,
|
||||
balanced: unpaid === 0 && unspent === 0,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = SettleUp;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Keyboard control for a dealer running this on a laptop.
|
||||
//
|
||||
// The phone has gestures; the desktop had a mouse and nothing else. These are the
|
||||
// same actions the bar already offers, reachable without moving a hand to the
|
||||
// trackpad between every decision — which is the whole point when the bar is being
|
||||
// tapped thirty times a hand.
|
||||
const Shortcuts = {
|
||||
// Deliberately single letters with no modifier: a dealer's spare hand rests on
|
||||
// the keyboard, and a chord is slower than reaching for the button would be.
|
||||
KEYS: {
|
||||
f: "fold",
|
||||
c: "checkOrCall",
|
||||
r: "raise",
|
||||
a: "allIn",
|
||||
" ": "advance",
|
||||
enter: "advance",
|
||||
u: "undo",
|
||||
escape: "cancel",
|
||||
},
|
||||
|
||||
// What each one does, in a dealer's words, for the legend in the sidebar.
|
||||
//
|
||||
// Lives beside KEYS because the two must not drift: F, C and R are printed on the
|
||||
// buttons they drive, but A and U were on nothing at all — a dealer who learned
|
||||
// the first three from the bar had no way to find out the other two existed. A
|
||||
// shortcut with no line here is a shortcut nobody can find, and there's a test
|
||||
// that says so.
|
||||
LEGEND: [
|
||||
{ keys: ["F"], action: "fold", what: "Fold" },
|
||||
{ keys: ["C"], action: "checkOrCall", what: "Check, or call the bet" },
|
||||
{ keys: ["R"], action: "raise", what: "Open the amount pad to bet or raise" },
|
||||
{ keys: ["A"], action: "allIn", what: "Put this player all-in" },
|
||||
{ keys: ["U"], action: "undo", what: "Take back the last action" },
|
||||
{ keys: ["Space", "Enter"], action: "advance", what: "Deal, or move to the next street" },
|
||||
{ keys: ["Esc"], action: "cancel", what: "Close the amount pad without betting" },
|
||||
],
|
||||
|
||||
mount(handlers) {
|
||||
this.handlers = handlers;
|
||||
document.addEventListener("keydown", e => this.onKey(e));
|
||||
},
|
||||
|
||||
// Nothing fires while the dealer is typing a name, an amount, or a note — the
|
||||
// "c" in "Cass" must not fold the table.
|
||||
isTyping() {
|
||||
const el = document.activeElement;
|
||||
if (!el) return false;
|
||||
const tag = el.tagName;
|
||||
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el.isContentEditable;
|
||||
},
|
||||
|
||||
// An overlay owns the keyboard while it's up. Asking the document which overlays
|
||||
// are open, rather than listing them, means a sheet added later can't quietly
|
||||
// start letting "f" fold the table behind it — the same reason Modal watches for
|
||||
// them instead of being told about each one.
|
||||
overlayOpen() {
|
||||
return [...document.querySelectorAll('[id$="Overlay"]')].some(el => !el.hidden);
|
||||
},
|
||||
|
||||
// Enter and Space are how a keyboard presses the control it is already sitting on.
|
||||
// Claiming them for "advance" didn't just block that press — it ran a different
|
||||
// action instead. Measured: Tab to Resolve Pot, press Enter, and the table dealt
|
||||
// the next hand. Space on the same button worked, because "advance" happened to
|
||||
// decline that time and the browser's own activation got through. A control that
|
||||
// does something else every other press is worse than one that does nothing.
|
||||
//
|
||||
// Only these two keys are handed back. The letter shortcuts stay live with a
|
||||
// button focused, because a button does nothing with "f" and the dealer's hand
|
||||
// shouldn't have to leave them.
|
||||
focusedControlOwnsKey(key) {
|
||||
if (key !== "enter" && key !== " ") return false;
|
||||
const el = document.activeElement;
|
||||
return !!(el && el.matches && el.matches('button, [role="button"], a[href], summary'));
|
||||
},
|
||||
|
||||
onKey(e) {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
|
||||
const key = e.key.length === 1 ? e.key.toLowerCase() : e.key.toLowerCase();
|
||||
const action = this.KEYS[key];
|
||||
if (!action) return;
|
||||
|
||||
if (this.focusedControlOwnsKey(key)) return;
|
||||
|
||||
// Escape is the exception: it should still close the raise pad even if focus
|
||||
// happens to be sitting in its amount field.
|
||||
if (this.isTyping() && !(action === "cancel")) return;
|
||||
if (this.overlayOpen()) return;
|
||||
|
||||
const handled = this.handlers[action] && this.handlers[action]();
|
||||
if (handled) e.preventDefault();
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = Shortcuts;
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
const SplitCalculator = {
|
||||
// Everything is done in cents to keep 0.1 + 0.2 out of a payout.
|
||||
toCents(amount) {
|
||||
return Math.round((Number(amount) || 0) * 100);
|
||||
return Money.toCents(amount);
|
||||
},
|
||||
|
||||
split(potAmount, ways, chipAmount) {
|
||||
@@ -40,11 +40,11 @@ const SplitCalculator = {
|
||||
|
||||
// Sanity check for the UI: what the split actually hands out has to equal the pot.
|
||||
accountedFor(result) {
|
||||
return (
|
||||
Math.round(result.each * 100) * result.players +
|
||||
Math.round(result.chip * 100) * result.oddChips +
|
||||
Math.round(result.dust * 100)
|
||||
) / 100;
|
||||
return Money.fromCents(
|
||||
Money.toCents(result.each) * result.players +
|
||||
Money.toCents(result.chip) * result.oddChips +
|
||||
Money.toCents(result.dust)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
||||
// Keeps the screen on while a hand is live.
|
||||
//
|
||||
// The dealer's phone spends the night face-up on the felt with nine people round it,
|
||||
// and the one thing it reliably does between the flop and the turn is dim, then lock.
|
||||
// Every hand, all night. A dozen rounds of work have gone into making the next step
|
||||
// readable at a glance, and none of it survives a black screen and a passcode with
|
||||
// four people waiting to know whether the bet is called.
|
||||
//
|
||||
// Held only while betting is actually open. A phone left on the table through a break,
|
||||
// a re-buy, or the twenty minutes where everyone is arguing about pizza sleeps exactly
|
||||
// like any other phone — which is the difference between a tool that respects a battery
|
||||
// and one a dealer stops trusting with their evening.
|
||||
|
||||
const WakeLock = {
|
||||
// The browser's promise to stay awake. Null whenever we aren't holding one, which
|
||||
// includes every moment the browser has taken it back on us — see `start()`.
|
||||
sentinel: null,
|
||||
|
||||
// What the last sync decided we want. Kept separate from `sentinel` because
|
||||
// requesting one is asynchronous: a hand can be dealt and folded out inside the
|
||||
// await, and the answer to "should we still be holding this" has to be the current
|
||||
// one rather than the one that was true when the request went out.
|
||||
wanted: false,
|
||||
|
||||
// One request at a time. `sync()` runs off renderAll, which fires many times a
|
||||
// second during a busy street; without this, two overlapping requests both get
|
||||
// past the "already holding one?" check and the second sentinel leaks — the screen
|
||||
// then stays awake after the hand ends, with nothing left holding a reference to
|
||||
// turn it off.
|
||||
pending: false,
|
||||
|
||||
// Whether the dealer has been told this is happening. Once per page load, not once
|
||||
// per hand: the same toast at the top of all forty hands of a session would be the
|
||||
// single most annoying thing the tool does.
|
||||
told: false,
|
||||
|
||||
// Set by main.js so the toast's "Turn off" can actually turn it off.
|
||||
hooks: null,
|
||||
|
||||
supported() {
|
||||
return typeof navigator !== "undefined" && "wakeLock" in navigator;
|
||||
},
|
||||
|
||||
// The whole decision, with no browser in it, so it can be tested as what it is:
|
||||
// a rule about when a poker table needs the screen.
|
||||
shouldHold(state) {
|
||||
if (!state) return false;
|
||||
if (state.keepScreenAwake === false) return false;
|
||||
return state.bettingOpen === true;
|
||||
},
|
||||
|
||||
// Called after anything that could start or end a hand. Safe to call constantly.
|
||||
sync(state) {
|
||||
this.wanted = this.supported() && this.shouldHold(state);
|
||||
if (this.wanted) this.acquire();
|
||||
else this.release();
|
||||
},
|
||||
|
||||
async acquire() {
|
||||
if (this.sentinel || this.pending) return;
|
||||
this.pending = true;
|
||||
try {
|
||||
const sentinel = await navigator.wakeLock.request("screen");
|
||||
// The hand can have ended while that was in flight. Letting this one go
|
||||
// immediately is the only correct answer; holding it "just until the next
|
||||
// sync" is how a phone stays lit on an empty table.
|
||||
if (!this.wanted) {
|
||||
try { await sentinel.release(); } catch { /* already gone */ }
|
||||
return;
|
||||
}
|
||||
this.sentinel = sentinel;
|
||||
// The browser releases this itself when the tab is hidden, and tells us so.
|
||||
// Without this listener our `sentinel` would be a stale object and the
|
||||
// re-acquire in `start()` would decline to do anything.
|
||||
sentinel.addEventListener("release", () => {
|
||||
if (this.sentinel === sentinel) this.sentinel = null;
|
||||
});
|
||||
this.tell();
|
||||
} catch {
|
||||
// Battery saver, a hidden tab, a browser that simply says no. All of these
|
||||
// are the browser doing its job, and none of them is worth an error at a
|
||||
// poker table — the tool works exactly as it did before, the screen just
|
||||
// dims the way the phone's own settings say it should.
|
||||
} finally {
|
||||
this.pending = false;
|
||||
}
|
||||
},
|
||||
|
||||
async release() {
|
||||
const sentinel = this.sentinel;
|
||||
this.sentinel = null;
|
||||
if (!sentinel) return;
|
||||
try { await sentinel.release(); } catch { /* already released */ }
|
||||
},
|
||||
|
||||
// Said once, at the moment it first becomes true, with the way out attached.
|
||||
//
|
||||
// The alternative was a permanent indicator in the header, and the header is the
|
||||
// one part of this page that has been fought over for crowding more than any other.
|
||||
// A dealer needs to know their phone is behaving unusually exactly once; after that
|
||||
// it is a setting, and settings live in the sidebar.
|
||||
tell() {
|
||||
if (this.told) return;
|
||||
this.told = true;
|
||||
if (typeof Toast === "undefined") return;
|
||||
Toast.show("Keeping the screen on while a hand is live.", {
|
||||
actionLabel: "Turn off",
|
||||
onAction: () => { if (this.hooks && this.hooks.turnOff) this.hooks.turnOff(); },
|
||||
});
|
||||
},
|
||||
|
||||
// A screen wake lock is not something you take once. The browser drops it whenever
|
||||
// the document stops being visible — switching apps to check a rule, the phone
|
||||
// locking on its own before the first hand was dealt, a notification pulled down —
|
||||
// and it never comes back on its own. A tool that requests it at the start of the
|
||||
// hand and assumes it still has it is a tool that works until the first interruption.
|
||||
start(getState) {
|
||||
if (!this.supported()) return;
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.visibilityState !== "visible") return;
|
||||
this.sync(getState());
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = WakeLock;
|
||||
@@ -0,0 +1,86 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
addHeadingAnchors();
|
||||
trackReadingProgress();
|
||||
highlightCurrentSection();
|
||||
setUpSidebarFilter();
|
||||
setUpMobileMenu();
|
||||
});
|
||||
|
||||
// Hover "#" links on headings so any section can be linked to directly.
|
||||
function addHeadingAnchors() {
|
||||
document.querySelectorAll('.soc-content h2[id], .soc-content h3[id]').forEach(heading => {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.className = 'soc-anchor';
|
||||
anchor.href = '#' + heading.id;
|
||||
anchor.textContent = '#';
|
||||
anchor.setAttribute('aria-label', 'Link to this section');
|
||||
heading.prepend(anchor);
|
||||
});
|
||||
}
|
||||
|
||||
function trackReadingProgress() {
|
||||
const bar = document.getElementById('socProgress');
|
||||
if (!bar) return;
|
||||
|
||||
const update = () => {
|
||||
const scrollable = document.documentElement.scrollHeight - window.innerHeight;
|
||||
bar.style.width = scrollable > 0 ? (window.scrollY / scrollable) * 100 + '%' : '0';
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', update, { passive: true });
|
||||
update();
|
||||
}
|
||||
|
||||
// Highlights the "On this page" entry for the section currently being read.
|
||||
function highlightCurrentSection() {
|
||||
const links = [...document.querySelectorAll('.soc-toc a[data-toc-id]')];
|
||||
if (links.length === 0) return;
|
||||
|
||||
const headings = links
|
||||
.map(link => document.getElementById(link.dataset.tocId))
|
||||
.filter(Boolean);
|
||||
|
||||
const update = () => {
|
||||
let current = headings[0];
|
||||
for (const heading of headings) {
|
||||
if (heading.getBoundingClientRect().top <= 120) current = heading;
|
||||
}
|
||||
links.forEach(link => link.classList.toggle('active', link.dataset.tocId === current?.id));
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', update, { passive: true });
|
||||
update();
|
||||
}
|
||||
|
||||
function setUpSidebarFilter() {
|
||||
const input = document.getElementById('socFilter');
|
||||
if (!input) return;
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
const query = input.value.trim().toLowerCase();
|
||||
document.querySelectorAll('.soc-nav-link').forEach(link => {
|
||||
link.classList.toggle('hidden', query !== '' && !link.dataset.filterText.includes(query));
|
||||
});
|
||||
document.querySelectorAll('.soc-nav-group').forEach(group => {
|
||||
const anyVisible = group.querySelector('.soc-nav-link:not(.hidden)');
|
||||
group.style.display = anyVisible ? '' : 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setUpMobileMenu() {
|
||||
const toggle = document.getElementById('socMenuToggle');
|
||||
const sidebar = document.getElementById('socSidebar');
|
||||
if (!toggle || !sidebar) return;
|
||||
|
||||
const setOpen = open => {
|
||||
sidebar.classList.toggle('open', open);
|
||||
toggle.setAttribute('aria-expanded', String(open));
|
||||
toggle.textContent = open ? 'Close' : 'Contents';
|
||||
};
|
||||
|
||||
toggle.addEventListener('click', () => setOpen(!sidebar.classList.contains('open')));
|
||||
sidebar.addEventListener('click', event => {
|
||||
if (event.target.closest('a')) setOpen(false);
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
// Saying who the blind is, before saying "cards".
|
||||
//
|
||||
// A dealer sets the table up out loud — button here, she's the small blind, he's the
|
||||
// big blind — and only then deals. The seat menu offers "Make small blind" for exactly
|
||||
// that, and it was answering by dealing the hand: betting opened, two stacks docked,
|
||||
// the action bar up and waiting on the seat after the big blind. The dealer had said
|
||||
// where the blinds go, and the tool heard "deal".
|
||||
//
|
||||
// The condition it was asking is `onlyBlindsIn`, which means "nothing has happened in
|
||||
// this hand except the blinds" — true of a hand one second old, and equally true of a
|
||||
// table in setup with no cards out and nothing committed. Those two need telling apart,
|
||||
// which is what `blindsArePosted` is for.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { Betting, HoldemState, makeTable, chipsOnTable } = require("./load");
|
||||
|
||||
describe("before the deal", () => {
|
||||
test("the blinds are not down, however much the table looks ready", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3] });
|
||||
assert.equal(state.bettingOpen, false);
|
||||
// The old condition, on its own, says yes here — this is the whole bug.
|
||||
assert.equal(Betting.onlyBlindsIn(state), true);
|
||||
assert.equal(Betting.blindsArePosted(state), false);
|
||||
});
|
||||
|
||||
test("a table straight out of setup, with nobody seated, is the same answer", () => {
|
||||
assert.equal(Betting.blindsArePosted(HoldemState.createDefault()), false);
|
||||
});
|
||||
|
||||
test("re-posting would have dealt the hand, which is why it is guarded", () => {
|
||||
// Not a rule, a demonstration: repostBlinds ends in beginHand. Calling it in
|
||||
// setup is the deal, and nothing about naming a blind should reach it.
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3] });
|
||||
Betting.repostBlinds(state);
|
||||
assert.equal(state.bettingOpen, true, "chips off two stacks and the action bar up");
|
||||
assert.notEqual(state.actingSeatIndex, null);
|
||||
});
|
||||
|
||||
test("naming a blind moves a badge and no chips", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3] });
|
||||
const before = chipsOnTable(state);
|
||||
|
||||
// What the seat menu does: pin the override, repost only if the blinds are down.
|
||||
state.blindOverride = { sbIndex: 3, bbIndex: 0 };
|
||||
if (Betting.blindsArePosted(state)) Betting.repostBlinds(state);
|
||||
|
||||
assert.equal(state.bettingOpen, false, "still setup — the dealer hasn't dealt");
|
||||
assert.equal(state.actingSeatIndex, null, "nobody is being asked to act");
|
||||
assert.deepEqual(state.seats.map(p => Betting.committed(p)), [0, 0, 0, 0, 0, 0]);
|
||||
assert.equal(chipsOnTable(state), before);
|
||||
|
||||
// And the badges say what the dealer just said.
|
||||
const blinds = HoldemState.getBlindSeats(state);
|
||||
assert.equal(blinds.sbIndex, 3);
|
||||
assert.equal(blinds.bbIndex, 0);
|
||||
});
|
||||
|
||||
test("and the hand that follows posts from where they were aimed", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], smallBlind: 1, bigBlind: 2 });
|
||||
state.blindOverride = { sbIndex: 3, bbIndex: 0 };
|
||||
if (Betting.blindsArePosted(state)) Betting.repostBlinds(state);
|
||||
|
||||
Betting.beginHand(state);
|
||||
assert.equal(Betting.committed(state.seats[3]), 1);
|
||||
assert.equal(Betting.committed(state.seats[0]), 2);
|
||||
});
|
||||
|
||||
test("giving the button away is a marker moving too", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3] });
|
||||
const before = chipsOnTable(state);
|
||||
|
||||
const willRepost = Betting.blindsArePosted(state);
|
||||
state.dealerSeatIndex = 2;
|
||||
if (willRepost) Betting.repostBlinds(state);
|
||||
|
||||
assert.equal(willRepost, false);
|
||||
assert.equal(state.bettingOpen, false);
|
||||
assert.equal(chipsOnTable(state), before);
|
||||
});
|
||||
});
|
||||
|
||||
describe("once the hand is dealt", () => {
|
||||
test("the blinds are down, and re-aiming them moves real chips", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
const before = chipsOnTable(state);
|
||||
Betting.beginHand(state);
|
||||
assert.equal(Betting.blindsArePosted(state), true);
|
||||
assert.equal(Betting.committed(state.seats[1]), 1);
|
||||
|
||||
state.blindOverride = { sbIndex: 3, bbIndex: 0 };
|
||||
if (Betting.blindsArePosted(state)) Betting.repostBlinds(state);
|
||||
|
||||
assert.equal(Betting.committed(state.seats[1]), 0, "the old small blind got their chip back");
|
||||
assert.equal(Betting.committed(state.seats[3]), 1);
|
||||
assert.equal(Betting.committed(state.seats[0]), 2);
|
||||
assert.equal(chipsOnTable(state), before, "no chips created or lost in the shuffle");
|
||||
});
|
||||
|
||||
test("but not once somebody has answered them", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
Betting.call(state, state.actingSeatIndex);
|
||||
assert.equal(Betting.blindsArePosted(state), false, "there is a call to unpick now");
|
||||
});
|
||||
|
||||
test("nor on a later street", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
state.street = "flop";
|
||||
assert.equal(Betting.blindsArePosted(state), false);
|
||||
});
|
||||
|
||||
test("and a misdealt hand goes back to being a table in setup", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
Betting.takeHandBack(state);
|
||||
assert.equal(Betting.blindsArePosted(state), false, "nothing is posted until the dealer deals again");
|
||||
});
|
||||
|
||||
test("as does a hand that has been paid out", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
Betting.sweepToHandTotals(state);
|
||||
HoldemState.startNextHand(state);
|
||||
assert.equal(Betting.blindsArePosted(state), false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
// Who the felt marks "ALL IN".
|
||||
//
|
||||
// The mark's whole value is that a dealer can scan the ring and see who can still be
|
||||
// bet at, so the test that matters most is the last one: the seats it marks are
|
||||
// exactly the seats the betting round refuses to ask for another chip.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { Betting, HoldemState, makeTable, chipsOnTable } = require("./load");
|
||||
|
||||
const CSS = fs.readFileSync(
|
||||
path.join(__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "css", "casino", "holdem.css"), "utf8");
|
||||
|
||||
// The declarations inside one rule, by its exact selector.
|
||||
const ruleBody = selector => {
|
||||
const at = CSS.indexOf(`\n${selector} {`);
|
||||
if (at < 0) return null;
|
||||
const open = CSS.indexOf("{", at);
|
||||
return CSS.slice(open + 1, CSS.indexOf("}", open));
|
||||
};
|
||||
|
||||
test("a player who moved all-in is marked", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
||||
state.seats[3].stack = 12;
|
||||
Betting.beginHand(state);
|
||||
Betting.allIn(state, state.actingSeatIndex);
|
||||
|
||||
assert.equal(Betting.isAllIn(state, state.seats[3]), true);
|
||||
assert.equal(state.seats[3].stack, 0);
|
||||
});
|
||||
|
||||
test("a player with chips behind is not", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||
Betting.beginHand(state);
|
||||
assert.equal(Betting.isAllIn(state, state.seats[0]), false);
|
||||
assert.equal(Betting.isAllIn(state, state.seats[1]), false);
|
||||
});
|
||||
|
||||
test("a busted player who was dealt in but has put nothing in is not", () => {
|
||||
// An empty stack alone isn't all-in — it's someone who shouldn't have been dealt
|
||||
// in. Calling that "ALL IN" on the felt would be a lie about a live pot.
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 });
|
||||
state.seats[2].stack = 0;
|
||||
assert.equal(Betting.isAllIn(state, state.seats[2]), false);
|
||||
});
|
||||
|
||||
test("a folded player with an empty stack is not", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 });
|
||||
state.seats[1].committed = 5;
|
||||
state.seats[1].stack = 0;
|
||||
state.seats[1].folded = true;
|
||||
assert.equal(Betting.isAllIn(state, state.seats[1]), false);
|
||||
});
|
||||
|
||||
test("an empty chair is not", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
|
||||
assert.equal(Betting.isAllIn(state, state.seats[4]), false);
|
||||
});
|
||||
|
||||
test("a seat with no stack of its own falls back to the table's and isn't marked", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 });
|
||||
state.defaultStack = 200;
|
||||
state.seats[2].stack = null;
|
||||
state.seats[2].committed = 10;
|
||||
assert.equal(Betting.isAllIn(state, state.seats[2]), false);
|
||||
});
|
||||
|
||||
test("the seats marked are exactly the ones the action passes over", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
||||
state.seats[1].stack = 8;
|
||||
state.seats[3].stack = 30;
|
||||
Betting.beginHand(state);
|
||||
|
||||
// Everyone in, two of them for less than the others can cover.
|
||||
for (let step = 0; step < 4; step++) Betting.allIn(state, state.actingSeatIndex);
|
||||
|
||||
const marked = state.seats
|
||||
.map((player, index) => (Betting.isAllIn(state, player) ? index : null))
|
||||
.filter(index => index !== null);
|
||||
const skipped = state.seats
|
||||
.map((player, index) =>
|
||||
player.inHand && !player.folded && Betting.nextToAct(state, index - 1) !== index ? index : null)
|
||||
.filter(index => index !== null);
|
||||
|
||||
assert.deepEqual(marked, skipped);
|
||||
assert.ok(marked.length > 0, "the scenario should actually produce all-in players");
|
||||
});
|
||||
|
||||
test("nobody is marked once the next hand starts", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 });
|
||||
state.seats[2].stack = 5;
|
||||
Betting.beginHand(state);
|
||||
Betting.allIn(state, state.actingSeatIndex);
|
||||
assert.ok(state.seats.some(player => Betting.isAllIn(state, player)));
|
||||
|
||||
// The short stack got there by winning nothing; give them chips back the way a
|
||||
// dealer would before the next deal.
|
||||
HoldemState.startNextHand(state);
|
||||
state.seats[2].stack = 50;
|
||||
assert.equal(state.seats.filter(player => Betting.isAllIn(state, player)).length, 0);
|
||||
});
|
||||
|
||||
test("marking nobody doesn't move a chip", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
||||
state.seats[3].stack = 12;
|
||||
Betting.beginHand(state);
|
||||
const before = chipsOnTable(state);
|
||||
state.seats.forEach(player => Betting.isAllIn(state, player));
|
||||
assert.equal(chipsOnTable(state), before);
|
||||
});
|
||||
|
||||
// ---- And the shape of the mark, which is the felt's problem ------------------
|
||||
//
|
||||
// "ALL IN" is the only thing that ever lands on the stack line with a space in it, and
|
||||
// a space is somewhere to break. Measured at 360x640 with twelve players all-in, it
|
||||
// wanted 36px and had 34: it broke onto a second line, and an 11px line became 22.
|
||||
//
|
||||
// That is not a typography complaint. The puck went from 55px tall to 66, and puck
|
||||
// height is what the seat ring is short of — at the ends of an ellipse the seats are
|
||||
// separated vertically, so 11px taller is 11px further into the seat next door. The
|
||||
// layout search then tried every width it has, found none that cleared, and fell back
|
||||
// to the narrowest, which is also the one that wraps hardest.
|
||||
//
|
||||
// So the rule every design system gives a badge applies here too: one line, always.
|
||||
describe("the mark is one line, because the ring cannot afford two", () => {
|
||||
test("a stack line can never wrap", () => {
|
||||
const body = ruleBody(".seatPuckStack");
|
||||
assert.ok(body, "the .seatPuckStack rule should exist");
|
||||
assert.match(body, /white-space:\s*nowrap/,
|
||||
"the stack line must not be allowed to wrap — a second line is 11px the ring hasn't got");
|
||||
});
|
||||
|
||||
test("and on the tightest pucks it gives up its letter-spacing to fit", () => {
|
||||
// Not wrapping alone only converts a wrap into a spill: a line that cannot wrap
|
||||
// and still doesn't fit paints over the seat beside it instead. The spacing is
|
||||
// 3.6px of the 36 it wanted, and it is the one of the mark's three signals that
|
||||
// can go — the warm colour and the bold weight stay.
|
||||
const base = ruleBody(".seatPuckStack.seatPuckAllIn");
|
||||
const tight = ruleBody(".seatsTight .seatPuckStack.seatPuckAllIn");
|
||||
assert.ok(base, "the base all-in rule should exist");
|
||||
assert.ok(tight, "the tight-tier all-in rule should exist");
|
||||
|
||||
const spacingOf = body => {
|
||||
const match = body.match(/letter-spacing:\s*([\d.]+)em/);
|
||||
return match ? Number(match[1]) : null;
|
||||
};
|
||||
const wide = spacingOf(base);
|
||||
const narrow = spacingOf(tight);
|
||||
assert.ok(wide !== null && narrow !== null, "both rules should set letter-spacing in em");
|
||||
assert.ok(narrow < wide,
|
||||
`the tight tier should space the mark more tightly than the ordinary one (${narrow} vs ${wide})`);
|
||||
|
||||
// The arithmetic that has to hold for it to fit at all: six characters at 10px
|
||||
// with 4px of padding either side and a 1px border, inside a 44px puck.
|
||||
const CONTENT_PX = 44 - 2 * 4 - 2 * 1;
|
||||
const inkAt = spacing => 32.4 + spacing * 10 * "ALL IN".length;
|
||||
assert.ok(inkAt(narrow) <= CONTENT_PX,
|
||||
`"ALL IN" must fit the narrowest puck: ${inkAt(narrow).toFixed(1)}px into ${CONTENT_PX}px`);
|
||||
assert.ok(inkAt(wide) > CONTENT_PX,
|
||||
"the ordinary spacing is what didn't fit — if it does now, this test is measuring the wrong thing");
|
||||
});
|
||||
|
||||
test("the two signals that survive are the ones doing the work", () => {
|
||||
// The spacing exists so "ALL IN" doesn't read as money at a glance. Dropping it
|
||||
// on the narrow pucks is only safe while the colour and the weight remain, so
|
||||
// they are pinned here rather than left to be tidied away later.
|
||||
const base = ruleBody(".seatPuckStack.seatPuckAllIn");
|
||||
assert.match(base, /color:\s*#ffcf6b/i, "the warm colour separates it from a grey stack");
|
||||
assert.match(base, /font-weight:\s*700/, "and the weight does the rest");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
// What the tool says out loud between taps.
|
||||
//
|
||||
// The action bar carried `aria-live="polite"` on the element whose entire contents
|
||||
// render() rebuilds every action, so a screen reader read the whole bar after every
|
||||
// single tap. Measured, after one:
|
||||
//
|
||||
// "PRE-FLOP Pot $15 9 players Bob · $250 behind 0:00 ↶ dealing the hand U
|
||||
// Fold Call $10 Raise"
|
||||
//
|
||||
// Nothing in that is wrong. It is just unusable at the speed a hand is dealt, and it
|
||||
// is the documented anti-pattern: a live region has to be a stable element whose text
|
||||
// changes, not a container that gets replaced.
|
||||
//
|
||||
// What a dealer running this by ear needs is who is on the clock and what it costs
|
||||
// them. These are the sentences.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { ActionBar, Betting, HoldemState, makeTable } = require("./load");
|
||||
|
||||
const barOn = (state) => {
|
||||
ActionBar.state = state;
|
||||
ActionBar.lastAnnouncement = undefined;
|
||||
return ActionBar;
|
||||
};
|
||||
|
||||
describe("before the cards are out", () => {
|
||||
test("a table that could be dealt says so", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3] });
|
||||
assert.equal(barOn(state).announcement(), "Ready to deal.");
|
||||
});
|
||||
|
||||
test("and an empty table says what it is, rather than nothing", () => {
|
||||
// This used to announce "" — silence being the honest answer when the bar was
|
||||
// hidden on a table nobody was sitting at. The bar isn't hidden any more: an
|
||||
// empty table is the one state a dealer meets before they know how any of this
|
||||
// works, so it says so and offers the next step.
|
||||
const state = HoldemState.createDefault();
|
||||
assert.equal(barOn(state).announcement(), "Nobody's at the table yet.");
|
||||
});
|
||||
|
||||
test("and a table with one named player has moved on from that", () => {
|
||||
const state = HoldemState.createDefault();
|
||||
state.seats[0].name = "Ann";
|
||||
assert.notEqual(barOn(state).announcement(), "Nobody's at the table yet.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("while a hand is running", () => {
|
||||
const dealt = () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], smallBlind: 1, bigBlind: 2 });
|
||||
state.seats.forEach((p, i) => { p.name = ["Ann", "Ben", "Cal", "Dee"][i] || ""; });
|
||||
Betting.beginHand(state);
|
||||
return state;
|
||||
};
|
||||
|
||||
test("names who is on the clock, what it costs, and the pot", () => {
|
||||
const state = dealt();
|
||||
const said = barOn(state).announcement();
|
||||
assert.match(said, /to act/);
|
||||
assert.match(said, /\$2 to call/);
|
||||
assert.match(said, /Pot \$3\./);
|
||||
});
|
||||
|
||||
test("it is the seat that is actually acting, not the one after it", () => {
|
||||
const state = dealt();
|
||||
const name = HoldemState.displayName(state.seats[state.actingSeatIndex], state.actingSeatIndex);
|
||||
assert.ok(barOn(state).announcement().startsWith(name + " to act"));
|
||||
});
|
||||
|
||||
test("says checked to them rather than $0 to call", () => {
|
||||
// A number nobody owes is a number a dealer has to stop and parse.
|
||||
const state = dealt();
|
||||
state.currentBet = 0;
|
||||
assert.match(barOn(state).announcement(), /checked to them/);
|
||||
assert.doesNotMatch(barOn(state).announcement(), /\$0 to call/);
|
||||
});
|
||||
|
||||
test("a street with nobody left to act says the street is done", () => {
|
||||
const state = dealt();
|
||||
state.actingSeatIndex = null;
|
||||
state.street = "flop";
|
||||
assert.equal(barOn(state).announcement(), "Flop betting is done.");
|
||||
});
|
||||
|
||||
test("and a finished hand points at the pot", () => {
|
||||
const state = dealt();
|
||||
state.seats.forEach((p, i) => { if (i !== 0) p.folded = true; });
|
||||
assert.equal(Betting.isHandOver(state), true);
|
||||
assert.equal(barOn(state).announcement(), "Hand over. Resolve the pot.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("how often it speaks", () => {
|
||||
// A live region that is handed the same sentence twice reads it out twice, and
|
||||
// render() runs for a great many reasons that have nothing to do with whose turn
|
||||
// it is — a resize, a panel opening, a note being typed.
|
||||
const stubTarget = () => {
|
||||
const el = { textContent: "" };
|
||||
global.document = { getElementById: id => (id === "actionAnnouncer" ? el : null) };
|
||||
return el;
|
||||
};
|
||||
|
||||
test("says nothing new when nothing changed", () => {
|
||||
const el = stubTarget();
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], smallBlind: 1, bigBlind: 2 });
|
||||
Betting.beginHand(state);
|
||||
const bar = barOn(state);
|
||||
|
||||
bar.announce();
|
||||
const first = el.textContent;
|
||||
assert.ok(first.length > 0);
|
||||
|
||||
el.textContent = "CLEARED"; // if announce() writes again, this comes back
|
||||
bar.announce();
|
||||
assert.equal(el.textContent, "CLEARED", "the same sentence was not written twice");
|
||||
delete global.document;
|
||||
});
|
||||
|
||||
test("but does when the action moves", () => {
|
||||
const el = stubTarget();
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], smallBlind: 1, bigBlind: 2 });
|
||||
Betting.beginHand(state);
|
||||
const bar = barOn(state);
|
||||
|
||||
bar.announce();
|
||||
const first = el.textContent;
|
||||
Betting.call(state, state.actingSeatIndex);
|
||||
bar.announce();
|
||||
|
||||
assert.notEqual(el.textContent, first);
|
||||
assert.match(el.textContent, /to act/);
|
||||
delete global.document;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
// Perceptual contrast (APCA), and how much of it each piece of text needs.
|
||||
//
|
||||
// The audit already had a contrast check and it passed, which is why this exists. That
|
||||
// check measures WCAG 2 contrast ratio, and WCAG 2's own authors say it is the wrong
|
||||
// instrument for this page: the ratio is a luminance gap, and a luminance gap
|
||||
// systematically *overstates* readability once one of the two colours is near black.
|
||||
// This table's page is #141414 and its panels are darker still, so every reading it
|
||||
// takes is on the side of the algorithm where it is least reliable. Text can clear
|
||||
// 4.5:1 and still read as a smudge.
|
||||
//
|
||||
// APCA asks the other question — can text of *this size and weight* be read on this
|
||||
// background — and answers it on a scale where a given number means the same
|
||||
// readability at either polarity. It replaces nothing: the WCAG check stays, because
|
||||
// that is the number an accessibility audit will ask for. This is the one that says
|
||||
// whether a dealer can read it in a dim room.
|
||||
//
|
||||
// Constants are 0.98G-4g, and the implementation is checked against the eight
|
||||
// published reference pairs in apcaMath.test.js. An unverified contrast checker is
|
||||
// worse than none, because it launders a guess into a number.
|
||||
|
||||
const MAIN_TRC = 2.4;
|
||||
const R_CO = 0.2126729, G_CO = 0.7151522, B_CO = 0.0721750;
|
||||
const NORM_BG = 0.56, NORM_TXT = 0.57, REV_TXT = 0.62, REV_BG = 0.65;
|
||||
const BLK_THRS = 0.022, BLK_CLMP = 1.414;
|
||||
const SCALE_BOW = 1.14, SCALE_WOB = 1.14;
|
||||
const LO_BOW_OFFSET = 0.027, LO_WOB_OFFSET = 0.027;
|
||||
const DELTA_Y_MIN = 0.0005, LO_CLIP = 0.001;
|
||||
|
||||
// Screen luminance. Note this is a straight exponent, not the sRGB piecewise curve —
|
||||
// APCA specifies the simple 2.4 power and the two are not interchangeable here.
|
||||
function sRGBtoY(rgb) {
|
||||
const s = c => Math.pow(c / 255.0, MAIN_TRC);
|
||||
return R_CO * s(rgb[0]) + G_CO * s(rgb[1]) + B_CO * s(rgb[2]);
|
||||
}
|
||||
|
||||
// Signed: positive is dark text on light, negative is light text on dark. The sign
|
||||
// carries the polarity, and polarity is not cosmetic — the same luminance pair reads
|
||||
// differently each way round, which is the whole reason this exists.
|
||||
function contrast(txtY, bgY) {
|
||||
txtY = txtY > BLK_THRS ? txtY : txtY + Math.pow(BLK_THRS - txtY, BLK_CLMP);
|
||||
bgY = bgY > BLK_THRS ? bgY : bgY + Math.pow(BLK_THRS - bgY, BLK_CLMP);
|
||||
|
||||
if (Math.abs(bgY - txtY) < DELTA_Y_MIN) return 0.0;
|
||||
|
||||
let sapc, out;
|
||||
if (bgY > txtY) {
|
||||
sapc = (Math.pow(bgY, NORM_BG) - Math.pow(txtY, NORM_TXT)) * SCALE_BOW;
|
||||
out = sapc < LO_CLIP ? 0.0 : sapc - LO_BOW_OFFSET;
|
||||
} else {
|
||||
sapc = (Math.pow(bgY, REV_BG) - Math.pow(txtY, REV_TXT)) * SCALE_WOB;
|
||||
out = sapc > -LO_CLIP ? 0.0 : sapc + LO_WOB_OFFSET;
|
||||
}
|
||||
return out * 100;
|
||||
}
|
||||
|
||||
function lc(textRgb, bgRgb) {
|
||||
return contrast(sRGBtoY(textRgb), sRGBtoY(bgRgb));
|
||||
}
|
||||
|
||||
// How much contrast this text needs, from the published APCA levels:
|
||||
//
|
||||
// Lc 90 fluent body text; and the floor for anything at 14px/400 or 12px/400
|
||||
// Lc 75 the minimum for a column of body text (18px/400, 16px/500, 14px/700)
|
||||
// Lc 60 the minimum for content text that isn't a column or block
|
||||
// Lc 45 larger, heavier text — headlines at 36px/400 or 24px/700
|
||||
// Lc 30 the absolute floor, and only for genuinely spot-read text
|
||||
//
|
||||
// Interpolating between those anchors rather than inventing a curve. Weight is folded
|
||||
// in as an effective size because that is how the published table behaves: bolder
|
||||
// strokes buy roughly the same readability as bigger ones, and 14px/700 sitting at the
|
||||
// same Lc 75 as 18px/400 is the anchor that fixes the exchange rate.
|
||||
function requiredLc(px, weight = 400) {
|
||||
const heavier = Math.max(0, (Number(weight) || 400) - 400) / 300; // 400..700 -> 0..1
|
||||
const effective = px * (1 + 0.28 * heavier); // 14px/700 ~= 18px/400
|
||||
|
||||
const anchors = [
|
||||
[12, 90],
|
||||
[14, 90],
|
||||
[15, 82],
|
||||
[18, 75],
|
||||
[24, 60],
|
||||
[36, 45],
|
||||
];
|
||||
if (effective <= anchors[0][0]) return anchors[0][1];
|
||||
const last = anchors[anchors.length - 1];
|
||||
if (effective >= last[0]) return last[1];
|
||||
for (let i = 1; i < anchors.length; i++) {
|
||||
const [x0, y0] = anchors[i - 1];
|
||||
const [x1, y1] = anchors[i];
|
||||
if (effective <= x1) {
|
||||
const t = (effective - x0) / (x1 - x0);
|
||||
return y0 + t * (y1 - y0);
|
||||
}
|
||||
}
|
||||
return last[1];
|
||||
}
|
||||
|
||||
module.exports = { sRGBtoY, contrast, lc, requiredLc };
|
||||
@@ -0,0 +1,141 @@
|
||||
// The contrast arithmetic, checked against the numbers its authors publish.
|
||||
//
|
||||
// A contrast checker that is wrong is worse than not having one, because it launders a
|
||||
// guess into a number and then a stylesheet gets tuned to it. The eight pairs below are
|
||||
// the reference values published with APCA 0.98G-4g; if the implementation drifts from
|
||||
// them by so much as a rounding change, these fail and nothing downstream is believed.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { lc, contrast, sRGBtoY, requiredLc } = require("./apca");
|
||||
|
||||
const hex = h => {
|
||||
h = h.replace("#", "");
|
||||
if (h.length === 3) h = h.split("").map(c => c + c).join("");
|
||||
return [0, 2, 4].map(i => parseInt(h.slice(i, i + 2), 16));
|
||||
};
|
||||
|
||||
describe("the published reference pairs", () => {
|
||||
// Text first, background second, exactly as the reference table lists them.
|
||||
const CASES = [
|
||||
["#888", "#fff", 63.056469930209424],
|
||||
["#fff", "#888", -68.54146436644962],
|
||||
["#000", "#aaa", 58.146262578561334],
|
||||
["#aaa", "#000", -56.24113336839742],
|
||||
["#123", "#def", 91.66830811481631],
|
||||
["#def", "#123", -93.06770049484275],
|
||||
["#123", "#444", 8.32326136957393],
|
||||
["#444", "#123", -7.526878460278154],
|
||||
];
|
||||
|
||||
CASES.forEach(([text, bg, expected]) => {
|
||||
test(`${text} on ${bg} is Lc ${expected.toFixed(2)}`, () => {
|
||||
assert.ok(Math.abs(lc(hex(text), hex(bg)) - expected) < 1e-9,
|
||||
`got ${lc(hex(text), hex(bg))}, want ${expected}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("polarity", () => {
|
||||
test("is signed, and the sign is not decoration", () => {
|
||||
// Dark text on light is positive, light text on dark negative. The two are not
|
||||
// mirror images — swapping them changes the magnitude as well as the sign,
|
||||
// which is the entire reason this is used instead of a ratio.
|
||||
const dark = lc(hex("#888"), hex("#fff"));
|
||||
const light = lc(hex("#fff"), hex("#888"));
|
||||
assert.ok(dark > 0 && light < 0);
|
||||
assert.ok(Math.abs(Math.abs(dark) - Math.abs(light)) > 5,
|
||||
"the same pair reads differently each way round");
|
||||
});
|
||||
|
||||
test("a colour on itself is nothing at all", () => {
|
||||
assert.equal(lc(hex("#2c2c2c"), hex("#2c2c2c")), 0);
|
||||
});
|
||||
|
||||
test("and so is a difference too small to see", () => {
|
||||
// Below deltaYmin the answer is zero rather than a tiny number, so a rounding
|
||||
// artefact can never be reported as contrast.
|
||||
assert.equal(lc([44, 44, 44], [44, 44, 45]), 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("luminance", () => {
|
||||
test("is the straight 2.4 power, not the sRGB piecewise curve", () => {
|
||||
// Worth pinning: the two are close enough to look right and different enough to
|
||||
// move a borderline reading. Mid grey under the piecewise curve is ~0.2159.
|
||||
assert.ok(Math.abs(sRGBtoY([128, 128, 128]) - Math.pow(128 / 255, 2.4)) < 1e-6);
|
||||
});
|
||||
|
||||
test("white is as close to 1 as the published coefficients get", () => {
|
||||
// 1.0000001, not 1. The three coefficients are given to seven places and sum to
|
||||
// a hair over one, and that is the specification rather than a mistake to
|
||||
// correct — the reference pairs above only come out exact with these values.
|
||||
// Asserted loosely on purpose: tightening this to 1e-12 fails on correct code.
|
||||
assert.ok(Math.abs(sRGBtoY([255, 255, 255]) - 1) < 1e-6);
|
||||
assert.equal(sRGBtoY([0, 0, 0]), 0);
|
||||
});
|
||||
|
||||
test("green carries most of it", () => {
|
||||
// Not arbitrary: it's why gold on green was the worst pairing on the felt.
|
||||
assert.ok(sRGBtoY([0, 255, 0]) > sRGBtoY([255, 0, 0]) + sRGBtoY([0, 0, 255]));
|
||||
});
|
||||
|
||||
test("contrast() takes luminances, lc() takes colours", () => {
|
||||
assert.equal(contrast(sRGBtoY(hex("#888")), sRGBtoY(hex("#fff"))), lc(hex("#888"), hex("#fff")));
|
||||
});
|
||||
});
|
||||
|
||||
describe("how much contrast a given piece of text needs", () => {
|
||||
test("the published anchors land where they should", () => {
|
||||
assert.equal(Math.round(requiredLc(12, 400)), 90);
|
||||
assert.equal(Math.round(requiredLc(14, 400)), 90);
|
||||
assert.equal(Math.round(requiredLc(18, 400)), 75);
|
||||
assert.equal(Math.round(requiredLc(24, 400)), 60);
|
||||
assert.equal(Math.round(requiredLc(36, 400)), 45);
|
||||
});
|
||||
|
||||
test("smaller text asks for more, never less", () => {
|
||||
for (let px = 9; px < 40; px++) {
|
||||
assert.ok(requiredLc(px, 400) >= requiredLc(px + 1, 400),
|
||||
`${px}px should ask for at least as much as ${px + 1}px`);
|
||||
}
|
||||
});
|
||||
|
||||
test("weight buys size, at the rate the published table sets", () => {
|
||||
// 14px/700 and 18px/400 sit at the same Lc 75 in the table, and that pair is
|
||||
// what fixes the exchange rate between weight and size here.
|
||||
assert.ok(Math.abs(requiredLc(14, 700) - requiredLc(18, 400)) < 3);
|
||||
assert.ok(requiredLc(14, 700) < requiredLc(14, 400), "bold asks for less than regular");
|
||||
});
|
||||
|
||||
test("it never runs off either end", () => {
|
||||
assert.equal(requiredLc(4, 400), 90);
|
||||
assert.equal(requiredLc(200, 900), 45);
|
||||
});
|
||||
});
|
||||
|
||||
describe("what the page's own colours measure", () => {
|
||||
// These are the readings the round was built on. Pinned so that a future change to
|
||||
// the tokens has to look at them rather than move them by accident.
|
||||
const PANEL = hex("#1a1a1a");
|
||||
|
||||
test("the greys these tokens replaced were below the floor", () => {
|
||||
assert.ok(Math.abs(lc(hex("#9a9a9a"), PANEL)) < 50, "the old muted grey");
|
||||
assert.ok(Math.abs(lc(hex("#8a8a8a"), PANEL)) < 40, "the old dim grey");
|
||||
});
|
||||
|
||||
test("and the ones that replaced them clear it", () => {
|
||||
assert.ok(Math.abs(lc(hex("#c8c8c8"), PANEL)) >= 60, "muted");
|
||||
assert.ok(Math.abs(lc(hex("#b4b4b4"), PANEL)) >= 58, "dim");
|
||||
});
|
||||
|
||||
test("the three tiers are still three tiers", () => {
|
||||
// The worry with lifting muted and dim is that they collapse into --text and the
|
||||
// page flattens. They don't: the spread is wider than it was.
|
||||
const text = Math.abs(lc(hex("#eeeeee"), PANEL));
|
||||
const muted = Math.abs(lc(hex("#c8c8c8"), PANEL));
|
||||
const dim = Math.abs(lc(hex("#b4b4b4"), PANEL));
|
||||
assert.ok(text > muted && muted > dim, "still ordered");
|
||||
assert.ok(text - dim > 30, "and still a real spread");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
// Chips the pot was priced with that nobody actually paid.
|
||||
//
|
||||
// There are two ways a contribution gets into this tool. Tapping the action moves
|
||||
// chips: the bet leaves the stack, sits in front of the player, and is swept into their
|
||||
// hand total at the end of the street. The other is the "without live betting" panel —
|
||||
// a base pot amount, or chips counted out on a seat — where the dealer is recording a
|
||||
// hand they are watching rather than driving, and nothing ever leaves a stack.
|
||||
//
|
||||
// The pot wizard prices both the same way and awarding it credits the winner, so in
|
||||
// that second mode the table simply grew.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { Money, Betting, HoldemState, chipsOnTable } = require("./load");
|
||||
|
||||
const basePotTable = (base = 50, stack = 200) => {
|
||||
const state = HoldemState.createDefault();
|
||||
HoldemState.resizeSeats(state, 4);
|
||||
state.basePotAmount = base;
|
||||
["Ann", "Ben", "Cal"].forEach((name, i) => {
|
||||
state.seats[i].name = name;
|
||||
state.seats[i].inHand = true;
|
||||
state.seats[i].stack = stack;
|
||||
state.seats[i].buyIns = stack;
|
||||
});
|
||||
return state;
|
||||
};
|
||||
|
||||
const pricedPot = (state) => state.seats.reduce(
|
||||
(sum, p) => sum + HoldemState.computeContribution(p, state.chipLegend, state.basePotAmount), 0);
|
||||
|
||||
describe("what nobody paid", () => {
|
||||
test("comes off the stacks that owe it", () => {
|
||||
const state = basePotTable();
|
||||
const collected = Betting.collectAssumedContributions(state);
|
||||
assert.equal(collected.total, 150);
|
||||
assert.deepEqual(collected.taken.map(row => row.name), ["Ann", "Ben", "Cal"]);
|
||||
assert.deepEqual(state.seats.slice(0, 3).map(p => p.stack), [150, 150, 150]);
|
||||
});
|
||||
|
||||
test("and lands where the rest of the tool looks for chips in the middle", () => {
|
||||
const state = basePotTable();
|
||||
Betting.collectAssumedContributions(state);
|
||||
assert.deepEqual(state.seats.slice(0, 3).map(p => p.amountOverride), [50, 50, 50]);
|
||||
assert.equal(HoldemState.unresolvedContributions(state), 150);
|
||||
});
|
||||
|
||||
test("chips counted out on a seat are money too", () => {
|
||||
const state = basePotTable(0);
|
||||
state.seats[0].chipCounts = { red: 4 }; // four $5 chips
|
||||
const collected = Betting.collectAssumedContributions(state);
|
||||
assert.equal(collected.total, 20);
|
||||
assert.equal(state.seats[0].stack, 180);
|
||||
});
|
||||
});
|
||||
|
||||
describe("what has already been paid", () => {
|
||||
test("a live bet is left exactly alone", () => {
|
||||
const state = basePotTable(0);
|
||||
state.smallBlind = 5;
|
||||
state.bigBlind = 10;
|
||||
Betting.beginHand(state);
|
||||
const before = state.seats.map(p => Betting.stackOf(state, p));
|
||||
|
||||
const collected = Betting.collectAssumedContributions(state);
|
||||
assert.equal(collected.total, 0, "the blinds are already off their stacks");
|
||||
assert.deepEqual(state.seats.map(p => Betting.stackOf(state, p)), before);
|
||||
});
|
||||
|
||||
test("nor is a swept street taken a second time", () => {
|
||||
const state = basePotTable(0);
|
||||
state.smallBlind = 5;
|
||||
state.bigBlind = 10;
|
||||
Betting.beginHand(state);
|
||||
Betting.sweepToHandTotals(state);
|
||||
const before = state.seats.map(p => Betting.stackOf(state, p));
|
||||
|
||||
assert.equal(Betting.collectAssumedContributions(state).total, 0);
|
||||
assert.deepEqual(state.seats.map(p => Betting.stackOf(state, p)), before);
|
||||
});
|
||||
|
||||
test("a base pot on top of a live bet only collects the difference", () => {
|
||||
// The base pot says "assume everyone is in for fifty". Somebody who has already
|
||||
// put twenty in owes thirty of it, not fifty.
|
||||
const state = basePotTable(50);
|
||||
state.seats[0].committed = 20;
|
||||
state.seats[0].stack = 180;
|
||||
const collected = Betting.collectAssumedContributions(state);
|
||||
const ann = collected.taken.find(row => row.name === "Ann");
|
||||
assert.equal(ann.amount, 30);
|
||||
assert.equal(state.seats[0].stack, 150, "fifty in the middle, either way");
|
||||
});
|
||||
});
|
||||
|
||||
describe("what it will not do", () => {
|
||||
test("take more than a player is holding", () => {
|
||||
const state = basePotTable(50);
|
||||
state.seats[1].stack = 20;
|
||||
Betting.collectAssumedContributions(state);
|
||||
assert.equal(state.seats[1].stack, 0, "all of it, and no further");
|
||||
assert.equal(state.seats[1].amountOverride, 20);
|
||||
});
|
||||
|
||||
test("touch an empty chair", () => {
|
||||
const state = basePotTable();
|
||||
const before = { ...state.seats[3] };
|
||||
Betting.collectAssumedContributions(state);
|
||||
assert.equal(state.seats[3].stack, before.stack);
|
||||
assert.equal(state.seats[3].amountOverride, before.amountOverride);
|
||||
});
|
||||
|
||||
test("or a seat that isn't in the hand", () => {
|
||||
// The base pot stands in for "everyone put the same in", and somebody sitting
|
||||
// out did not.
|
||||
const state = basePotTable();
|
||||
state.seats[2].inHand = false;
|
||||
const collected = Betting.collectAssumedContributions(state);
|
||||
assert.deepEqual(collected.taken.map(row => row.name), ["Ann", "Ben"]);
|
||||
assert.equal(state.seats[2].stack, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the whole hand", () => {
|
||||
test("conserves the chips on the table, which it did not before", () => {
|
||||
const state = basePotTable();
|
||||
const total = chipsOnTable(state);
|
||||
const pot = pricedPot(state);
|
||||
|
||||
Betting.collectAssumedContributions(state);
|
||||
Betting.creditWinnings(state, [{ id: state.seats[0].id, label: "Ann", amount: pot }]);
|
||||
HoldemState.startNextHand(state);
|
||||
|
||||
assert.equal(chipsOnTable(state), total, "six hundred before, six hundred after");
|
||||
assert.deepEqual(state.seats.slice(0, 3).map(p => p.stack), [300, 150, 150]);
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
});
|
||||
|
||||
test("and the bank check stays balanced all the way through", () => {
|
||||
const state = basePotTable();
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
Betting.collectAssumedContributions(state);
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true, "chips are in the middle, not gone");
|
||||
Betting.creditWinnings(state, [{ id: state.seats[0].id, label: "Ann", amount: 150 }]);
|
||||
HoldemState.startNextHand(state);
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
});
|
||||
|
||||
test("a live-betting hand is unchanged by any of this", () => {
|
||||
const state = basePotTable(0);
|
||||
state.smallBlind = 5;
|
||||
state.bigBlind = 10;
|
||||
const total = chipsOnTable(state);
|
||||
Betting.beginHand(state);
|
||||
[3, 0, 1, 2].forEach(() => { if (state.actingSeatIndex !== null) Betting.call(state, state.actingSeatIndex); });
|
||||
Betting.sweepToHandTotals(state);
|
||||
const pot = HoldemState.unresolvedContributions(state);
|
||||
|
||||
Betting.collectAssumedContributions(state);
|
||||
Betting.creditWinnings(state, [{ id: state.seats[0].id, label: "Ann", amount: pot }]);
|
||||
HoldemState.startNextHand(state);
|
||||
|
||||
assert.equal(chipsOnTable(state), total);
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
});
|
||||
|
||||
test("collecting twice takes nothing the second time", () => {
|
||||
// The pot wizard can be opened, cancelled and opened again.
|
||||
const state = basePotTable();
|
||||
Betting.collectAssumedContributions(state);
|
||||
const stacks = state.seats.map(p => p.stack);
|
||||
const again = Betting.collectAssumedContributions(state);
|
||||
assert.equal(again.total, 0);
|
||||
assert.deepEqual(state.seats.map(p => p.stack), stacks);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
// The sums the contrast check runs on, tested away from a browser.
|
||||
//
|
||||
// The check itself needs a page to look at, but the arithmetic underneath it doesn't —
|
||||
// and the arithmetic is the part that decides whether a real fault is reported or
|
||||
// waved through. Faded text used to be filed as "not checked" rather than measured;
|
||||
// this is the compositing that replaced that.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const LayoutAudit = require("./uiAudit");
|
||||
|
||||
const WHITE = { r: 255, g: 255, b: 255, a: 1 };
|
||||
const BLACK = { r: 0, g: 0, b: 0, a: 1 };
|
||||
const PAGE = { r: 28, g: 28, b: 28, a: 1 }; // the felt page behind everything
|
||||
const PUCK = { r: 44, g: 44, b: 44, a: 1 }; // a seat's own background
|
||||
|
||||
describe("the ratio itself", () => {
|
||||
test("black on white is the full 21:1, either way round", () => {
|
||||
assert.ok(Math.abs(LayoutAudit.contrastRatio(BLACK, WHITE) - 21) < 0.01);
|
||||
assert.ok(Math.abs(LayoutAudit.contrastRatio(WHITE, BLACK) - 21) < 0.01);
|
||||
});
|
||||
|
||||
test("a colour against itself is 1:1", () => {
|
||||
assert.ok(Math.abs(LayoutAudit.contrastRatio(PUCK, PUCK) - 1) < 0.001);
|
||||
});
|
||||
});
|
||||
|
||||
describe("text on a box that is being faded", () => {
|
||||
// opacity on a box fades the box and its text together onto whatever is behind
|
||||
// it — so a dimmed seat is not grey text on a dark puck, it is a faded puck and
|
||||
// faded text both landing on the page.
|
||||
const fade = alpha => ({ alpha, behind: PAGE });
|
||||
|
||||
test("at full strength nothing is composited", () => {
|
||||
const painted = LayoutAudit.asPainted(WHITE, PUCK, { alpha: 1, behind: PAGE });
|
||||
assert.deepEqual(painted.fg, WHITE);
|
||||
assert.deepEqual(painted.bg, PUCK);
|
||||
});
|
||||
|
||||
test("fading moves both the text and its background, not just the text", () => {
|
||||
const painted = LayoutAudit.asPainted(WHITE, PUCK, fade(0.7));
|
||||
assert.ok(painted.fg.r < WHITE.r, "the text dims");
|
||||
assert.ok(painted.bg.r < PUCK.r, "and so does what it sits on");
|
||||
assert.ok(painted.bg.r > PAGE.r, "but not all the way to the page");
|
||||
});
|
||||
|
||||
test("which is what stops a fade being reported as a fault it isn't", () => {
|
||||
// Dimming light text on a dark puck over a dark page costs contrast, but far
|
||||
// less than measuring the faded text against the *unfaded* puck would claim.
|
||||
const honest = LayoutAudit.asPainted(WHITE, PUCK, fade(0.7));
|
||||
const wrong = { fg: LayoutAudit.over({ ...WHITE, a: 0.7 }, PUCK), bg: PUCK };
|
||||
assert.ok(LayoutAudit.contrastRatio(honest.fg, honest.bg) >
|
||||
LayoutAudit.contrastRatio(wrong.fg, wrong.bg));
|
||||
});
|
||||
|
||||
test("a fade all the way out lands everything on the page behind", () => {
|
||||
const painted = LayoutAudit.asPainted(WHITE, PUCK, fade(0));
|
||||
assert.deepEqual(painted.fg, PAGE);
|
||||
assert.deepEqual(painted.bg, PAGE);
|
||||
});
|
||||
|
||||
test("with nothing known to be behind it, the fade is left alone", () => {
|
||||
// Better to measure the unfaded pair than to invent a backdrop.
|
||||
const painted = LayoutAudit.asPainted(WHITE, PUCK, { alpha: 0.5, behind: null });
|
||||
assert.deepEqual(painted.fg, WHITE);
|
||||
assert.deepEqual(painted.bg, PUCK);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a fade makes small grey text worse, which is the fault it went looking for", () => {
|
||||
// Deliberately not asserting a particular CSS colour here: the page's own greys are
|
||||
// measured in the browser, against the real gradients, where they live. What this
|
||||
// pins is the direction — that dimming a box costs its text contrast, which is why
|
||||
// filing faded text as "not checked" hid faults rather than avoiding false ones.
|
||||
test("the same grey scores lower once its box is dimmed", () => {
|
||||
const grey = LayoutAudit.parseColour("rgb(138, 138, 138)");
|
||||
const solid = LayoutAudit.asPainted(grey, PUCK, { alpha: 1, behind: PAGE });
|
||||
const dimmed = LayoutAudit.asPainted(grey, PUCK, { alpha: 0.7, behind: PAGE });
|
||||
assert.ok(LayoutAudit.contrastRatio(dimmed.fg, dimmed.bg) <
|
||||
LayoutAudit.contrastRatio(solid.fg, solid.bg));
|
||||
});
|
||||
|
||||
test("and a lighter grey scores higher at the same fade", () => {
|
||||
const at = colour => {
|
||||
const painted = LayoutAudit.asPainted(LayoutAudit.parseColour(colour), PUCK, { alpha: 0.7, behind: PAGE });
|
||||
return LayoutAudit.contrastRatio(painted.fg, painted.bg);
|
||||
};
|
||||
assert.ok(at("rgb(174, 174, 174)") > at("rgb(138, 138, 138)"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("reading a colour off the page", () => {
|
||||
test("rgb and rgba both come back as numbers", () => {
|
||||
assert.deepEqual(LayoutAudit.parseColour("rgb(1, 2, 3)"), { r: 1, g: 2, b: 3, a: 1 });
|
||||
assert.deepEqual(LayoutAudit.parseColour("rgba(1, 2, 3, 0.5)"), { r: 1, g: 2, b: 3, a: 0.5 });
|
||||
});
|
||||
|
||||
test("a gradient gives up every stop, because text could be over any of them", () => {
|
||||
const stops = LayoutAudit.gradientStops("linear-gradient(rgb(203, 170, 240), rgb(169, 127, 214))");
|
||||
assert.equal(stops.length, 2);
|
||||
assert.deepEqual(stops[0], { r: 203, g: 170, b: 240, a: 1 });
|
||||
assert.deepEqual(stops[1], { r: 169, g: 127, b: 214, a: 1 });
|
||||
});
|
||||
|
||||
test("no background image, no stops", () => {
|
||||
assert.deepEqual(LayoutAudit.gradientStops("none"), []);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,224 @@
|
||||
// The banker's job, which is a different job from the dealer's: every chip on the
|
||||
// table is a promise to pay cash for it later, and somebody has to be able to check
|
||||
// that the promise can be kept before the last two players try to cash out.
|
||||
const { test, describe } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { Betting, HoldemState, Money, makeTable } = require("./load");
|
||||
|
||||
const seat = (state, index, name, cash, chips) => {
|
||||
const p = state.seats[index];
|
||||
p.name = name; p.inHand = true; p.buyIns = cash; p.stack = chips;
|
||||
return p;
|
||||
};
|
||||
|
||||
describe("everyone with money in the game is accounted for", () => {
|
||||
test("a player who has bought in but played no logged hand still appears", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 0 });
|
||||
seat(state, 0, "Ann", 100, 100);
|
||||
seat(state, 1, "Ben", 100, 100);
|
||||
seat(state, 2, "Cal", 200, 200); // just sat down
|
||||
|
||||
state.seats[0].amountOverride = 10;
|
||||
state.seats[1].amountOverride = 10;
|
||||
HoldemState.recordHand(state, [{ id: state.seats[0].id, label: "Ann", amount: 20 }]);
|
||||
state.seats[0].amountOverride = null;
|
||||
state.seats[1].amountOverride = null;
|
||||
|
||||
const names = HoldemState.sessionStandings(state).map(r => r.name);
|
||||
assert.ok(names.includes("Cal"), `Cal handed over $200 and is missing: ${names.join(", ")}`);
|
||||
});
|
||||
|
||||
test("their row reads as no hands played, not as a loss", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||
seat(state, 0, "Ann", 100, 100);
|
||||
seat(state, 1, "Ben", 200, 200);
|
||||
|
||||
const cal = HoldemState.sessionStandings(state).find(r => r.name === "Ben");
|
||||
assert.equal(cal.in, 0);
|
||||
assert.equal(cal.won, 0);
|
||||
assert.equal(cal.net, 0);
|
||||
assert.equal(cal.boughtIn, 200);
|
||||
assert.equal(cal.cashPosition, 0, "level: they've bought chips and still hold them");
|
||||
});
|
||||
|
||||
test("an empty chair is not dragged into the settle-up", () => {
|
||||
const state = makeTable({ seatCount: 9, seated: [0], stack: 0 });
|
||||
seat(state, 0, "Ann", 100, 100);
|
||||
const rows = HoldemState.sessionStandings(state);
|
||||
assert.equal(rows.length, 1);
|
||||
});
|
||||
|
||||
test("somebody who has cashed out completely still shows what they took", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||
seat(state, 0, "Ann", 100, 0); // handed every chip back
|
||||
seat(state, 1, "Ben", 100, 200);
|
||||
const ann = HoldemState.sessionStandings(state).find(r => r.name === "Ann");
|
||||
assert.equal(ann.cashPosition, -100, "down the whole buy-in");
|
||||
});
|
||||
});
|
||||
|
||||
describe("does the cash box cover the chips on the table", () => {
|
||||
test("a straight game balances", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 0 });
|
||||
seat(state, 0, "Ann", 100, 60);
|
||||
seat(state, 1, "Ben", 100, 140);
|
||||
seat(state, 2, "Cal", 100, 100);
|
||||
|
||||
const bank = HoldemState.bankReconciliation(state);
|
||||
assert.equal(bank.cashIn, 300);
|
||||
assert.equal(bank.chipsInPlay, 300);
|
||||
assert.equal(bank.difference, 0);
|
||||
assert.equal(bank.balanced, true);
|
||||
});
|
||||
|
||||
test("chips outrunning the cash box is caught before cash-out", () => {
|
||||
// The 2am failure: the last players out can't be paid.
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 0 });
|
||||
seat(state, 0, "Ann", 100, 100);
|
||||
seat(state, 1, "Ben", 100, 100);
|
||||
seat(state, 2, "Cal", 100, 260); // took chips for a rebuy nobody wrote down
|
||||
|
||||
const bank = HoldemState.bankReconciliation(state);
|
||||
assert.equal(bank.cashIn, 300);
|
||||
assert.equal(bank.chipsInPlay, 460);
|
||||
assert.equal(bank.difference, 160);
|
||||
assert.equal(bank.balanced, false);
|
||||
});
|
||||
|
||||
test("cash taken for chips never handed over is caught too", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||
seat(state, 0, "Ann", 200, 100); // paid 200, only got 100 in chips
|
||||
seat(state, 1, "Ben", 100, 100);
|
||||
|
||||
const bank = HoldemState.bankReconciliation(state);
|
||||
assert.equal(bank.difference, -100);
|
||||
assert.equal(bank.balanced, false);
|
||||
});
|
||||
|
||||
test("chips in the middle still count as chips in play", () => {
|
||||
// Money in the pot is somebody's; it just hasn't been won yet. Leaving it out
|
||||
// would report a shortfall every time a hand was open — which is nearly always.
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], dealerSeatIndex: 0 });
|
||||
state.seats.forEach(p => { if (p.inHand) p.buyIns = 100; });
|
||||
|
||||
const before = HoldemState.bankReconciliation(state);
|
||||
assert.equal(before.balanced, true);
|
||||
|
||||
Betting.beginHand(state); // blinds go in
|
||||
Betting.raiseTo(state, 0, 25); // and a raise
|
||||
Betting.sweepToHandTotals(state); // banked into the pot
|
||||
|
||||
const during = HoldemState.bankReconciliation(state);
|
||||
assert.equal(during.balanced, true, "an open pot is not a discrepancy");
|
||||
assert.equal(during.chipsInPlay, before.chipsInPlay);
|
||||
});
|
||||
|
||||
test("a whole session of play never knocks the books out", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
state.seats.forEach(p => { if (p.inHand) p.buyIns = 100; });
|
||||
|
||||
for (let hand = 0; hand < 6; hand++) {
|
||||
Betting.beginHand(state);
|
||||
Betting.liveSeats(state).forEach(s => {
|
||||
if (Betting.toCall(state, s.player) > 0) Betting.call(state, s.index);
|
||||
});
|
||||
Betting.sweepToHandTotals(state);
|
||||
|
||||
const pot = HoldemState.totalPot(state);
|
||||
const winner = Betting.liveSeats(state)[0];
|
||||
const payouts = [{ id: winner.player.id, label: "w", amount: pot }];
|
||||
HoldemState.recordHand(state, payouts);
|
||||
Betting.creditWinnings(state, payouts);
|
||||
HoldemState.startNextHand(state);
|
||||
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true, `books off after hand ${hand + 1}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("a rebuy keeps the books straight, a chip grab doesn't", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||
seat(state, 0, "Ann", 100, 100);
|
||||
seat(state, 1, "Ben", 100, 100);
|
||||
|
||||
// A proper rebuy: cash to the banker, chips to the player.
|
||||
state.seats[0].buyIns += 100;
|
||||
state.seats[0].stack += 100;
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
|
||||
// Chips taken without paying.
|
||||
state.seats[1].stack += 50;
|
||||
const bank = HoldemState.bankReconciliation(state);
|
||||
assert.equal(bank.balanced, false);
|
||||
assert.equal(bank.difference, 50);
|
||||
});
|
||||
|
||||
test("nothing to reconcile before anybody has bought in", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [], stack: 0 });
|
||||
const bank = HoldemState.bankReconciliation(state);
|
||||
assert.equal(bank.cashIn, 0);
|
||||
assert.equal(bank.chipsInPlay, 0);
|
||||
assert.equal(bank.balanced, true);
|
||||
});
|
||||
|
||||
test("float dust is not a discrepancy", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||
seat(state, 0, "Ann", 0.1 + 0.2, 0.3);
|
||||
seat(state, 1, "Ben", 10, 10);
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the number the banker counts out", () => {
|
||||
// The standings had two signed columns and neither was an amount to pay. A player
|
||||
// up $140 on a $340 stack read "$140" under a heading saying "Cash out", at the
|
||||
// exact moment somebody was counting money onto the table.
|
||||
test("chips in front is what they are holding, not what they are up", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 340 });
|
||||
state.seats[0].name = "Ann";
|
||||
state.seats[0].buyIns = 200;
|
||||
state.seats[1].buyIns = 200;
|
||||
state.seats[1].stack = 60;
|
||||
|
||||
const rows = HoldemState.sessionStandings(state);
|
||||
const ann = rows.find(r => r.name === "Ann");
|
||||
assert.equal(ann.chipsInFront, 340, "the cash to count out");
|
||||
assert.equal(ann.cashPosition, 140, "and separately, that they are up $140");
|
||||
});
|
||||
|
||||
test("a player who is down still has chips to be handed", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 60 });
|
||||
state.seats[0].name = "Cal";
|
||||
state.seats.forEach(p => { p.buyIns = 200; });
|
||||
|
||||
const cal = HoldemState.sessionStandings(state).find(r => r.name === "Cal");
|
||||
assert.equal(cal.chipsInFront, 60, "they leave with $60 of chips");
|
||||
assert.equal(cal.cashPosition, -140, "having lost $140 of the $200 they brought");
|
||||
});
|
||||
|
||||
test("chips still out in the middle are counted as theirs", () => {
|
||||
// Cashing out mid-hand isn't a thing, but the panel is read mid-hand constantly
|
||||
// and a stack that drops by the size of a bet looks like money going missing.
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 200 });
|
||||
state.seats[0].name = "Dee";
|
||||
state.seats.forEach(p => { p.buyIns = 200; });
|
||||
Betting.beginHand(state);
|
||||
Betting.raiseTo(state, 0, 50);
|
||||
|
||||
const dee = HoldemState.sessionStandings(state).find(r => r.name === "Dee");
|
||||
assert.equal(dee.chipsInFront, 200, "stack plus what's in the middle for them");
|
||||
assert.equal(dee.cashPosition, 0, "so they are still level");
|
||||
});
|
||||
|
||||
test("chips and up/down always differ by exactly the buy-in", () => {
|
||||
const state = makeTable({ seatCount: 9, seated: [0, 1, 2, 3], stack: 100 });
|
||||
[340, 150, 60, 250].forEach((chips, i) => {
|
||||
state.seats[i].stack = chips;
|
||||
state.seats[i].buyIns = 200 + i * 50;
|
||||
});
|
||||
|
||||
HoldemState.sessionStandings(state).forEach(row => {
|
||||
assert.equal(Money.round(row.chipsInFront - row.boughtIn), row.cashPosition,
|
||||
`${row.name}'s two columns should tell one story`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,473 @@
|
||||
// Where a player's bet sits on the felt.
|
||||
//
|
||||
// The chips were the last thing on the table nobody had measured. They were laid out on
|
||||
// a ring of their own at a fixed 27/44 of the seat's reach — a fraction picked when the
|
||||
// seat ring was itself fixed — and the choice between that ring and clinging to the
|
||||
// puck was made by asking how wide the *window* was. Neither of those is a question
|
||||
// about the felt. A phone held sideways is 844px across and gives the felt 117px of
|
||||
// height: it passed the width test, took the ring, and every chip on it was painted
|
||||
// behind a seat. The seats carry `z-index: 1` and the bets layer carries none, so a
|
||||
// chip under a puck is not dimmed or half-hidden — it is gone, and with it the only
|
||||
// number saying what that player has in.
|
||||
//
|
||||
// Everything below is geometry, so it runs without a browser. The felts are the ones
|
||||
// measured on real screens: 692x117 is a phone on its side, 300x216 a phone upright.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { Render } = require("./load");
|
||||
|
||||
const RADII = { rx: 44, ry: 42 };
|
||||
const PUCK = { width: 96, height: 64 };
|
||||
const CHIP = { width: 44, height: 22 };
|
||||
|
||||
const felt = (width, height) => ({ width, height });
|
||||
const spacingOn = (box, puck = PUCK, chip = CHIP) =>
|
||||
({ box, puck, chip, clearance: Render.BET_CLEARANCE_PX });
|
||||
|
||||
// A chip's centre in pixels on a given felt.
|
||||
const chipPixels = (index, count, box, spacing, radii = RADII) => {
|
||||
const pos = Render.betPosition(index, count, box.width / box.height, radii, spacing);
|
||||
return { x: (pos.left / 100) * box.width, y: (pos.top / 100) * box.height };
|
||||
};
|
||||
|
||||
const seatPixels = (index, count, box, radii = RADII) => {
|
||||
const pos = Render.seatPosition(index, count, box.width / box.height, radii);
|
||||
return { x: (pos.left / 100) * box.width, y: (pos.top / 100) * box.height };
|
||||
};
|
||||
|
||||
// Two axis-aligned boxes, by their centres. The same test the seats settle among
|
||||
// themselves — see seatFootprint.test.js — because it is the same question.
|
||||
const boxesOverlap = (a, b, sizeA, sizeB) =>
|
||||
Math.abs(a.x - b.x) < (sizeA.width + sizeB.width) / 2 - 0.5 &&
|
||||
Math.abs(a.y - b.y) < (sizeA.height + sizeB.height) / 2 - 0.5;
|
||||
|
||||
describe("reaching the edge of a box", () => {
|
||||
test("straight out the side is half the width, straight up half the height", () => {
|
||||
assert.equal(Render.edgeReach(96, 64, 1, 0), 48);
|
||||
assert.equal(Render.edgeReach(96, 64, 0, -1), 32);
|
||||
assert.equal(Render.edgeReach(96, 64, -1, 0), 48);
|
||||
});
|
||||
|
||||
test("a corner is whichever edge the ray meets first", () => {
|
||||
// 45° out of a 96x64 box: the top and bottom are nearer than the sides, so the
|
||||
// ray leaves through one of those. A single radius — half the diagonal, 57.7 —
|
||||
// would put a chip 25px further out than it needs to be, which on a 117px felt
|
||||
// is the difference between clear of the puck and off the table.
|
||||
const diagonal = Render.edgeReach(96, 64, Math.SQRT1_2, Math.SQRT1_2);
|
||||
assert.ok(Math.abs(diagonal - 32 / Math.SQRT1_2) < 1e-9, `got ${diagonal}`);
|
||||
assert.ok(diagonal < 48, "and never further than the wider half-edge");
|
||||
});
|
||||
|
||||
test("a square is the same in every direction it is asked about", () => {
|
||||
const straight = Render.edgeReach(50, 50, 0, 1);
|
||||
assert.equal(Render.edgeReach(50, 50, 1, 0), straight);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getting one box clear of another", () => {
|
||||
const PUCK96 = { width: 96, height: 88 };
|
||||
const CHIP50 = { width: 50, height: 21 };
|
||||
|
||||
// Travel `t` along `u` from the puck's centre, then ask whether the two boxes are
|
||||
// apart. This is the question clearanceReach is supposed to answer, asked the long
|
||||
// way round so the answer can be checked rather than restated.
|
||||
const apart = (t, ux, uy) =>
|
||||
Math.abs(t * ux) >= (PUCK96.width + CHIP50.width) / 2 - 1e-9 ||
|
||||
Math.abs(t * uy) >= (PUCK96.height + CHIP50.height) / 2 - 1e-9;
|
||||
|
||||
test("straight up and down it is what adding the two half-heights gives", () => {
|
||||
assert.equal(Render.clearanceReach(PUCK96, CHIP50, 0, -1), (88 + 21) / 2);
|
||||
assert.equal(Render.clearanceReach(PUCK96, CHIP50, 1, 0), (96 + 50) / 2);
|
||||
});
|
||||
|
||||
test("on a diagonal it is more than the two reaches added together", () => {
|
||||
// The bug this replaced. Two axis-aligned boxes are clear when they are apart on
|
||||
// one axis, and the travel that achieves that is the reach of the box you get by
|
||||
// adding their widths and their heights — which is never less, and on a diagonal
|
||||
// is more, than adding the two boxes' own reaches. At 768x1024 the difference was
|
||||
// 11px, and 11px is how far inside its own player's puck the chip came to rest.
|
||||
const dirs = [[0.83, 0.56], [0.6, 0.8], [0.9, 0.44], [Math.SQRT1_2, Math.SQRT1_2]];
|
||||
let shortfalls = 0;
|
||||
dirs.forEach(([ux, uy]) => {
|
||||
const summed = Render.edgeReach(PUCK96.width, PUCK96.height, ux, uy) +
|
||||
Render.edgeReach(CHIP50.width, CHIP50.height, ux, uy);
|
||||
const proper = Render.clearanceReach(PUCK96, CHIP50, ux, uy);
|
||||
assert.ok(proper >= summed - 1e-9, `${ux},${uy}: ${proper} < ${summed}`);
|
||||
if (proper <= summed + 1e-9) return;
|
||||
// Where they differ, the old answer was not merely smaller — it was wrong:
|
||||
// travelling that far leaves the two boxes still on top of each other.
|
||||
shortfalls++;
|
||||
assert.ok(!apart(summed, ux, uy),
|
||||
`${ux},${uy}: adding the reaches gave ${summed.toFixed(1)}, which is enough after all`);
|
||||
});
|
||||
assert.ok(shortfalls >= 2, "expected the two to actually differ on some of these");
|
||||
});
|
||||
|
||||
test("and it is exactly enough — a hair less is not", () => {
|
||||
const dirs = [[0, -1], [1, 0], [0.83, 0.56], [0.6, -0.8], [-0.9, 0.44]];
|
||||
dirs.forEach(([ux, uy]) => {
|
||||
const t = Render.clearanceReach(PUCK96, CHIP50, ux, uy);
|
||||
assert.ok(apart(t, ux, uy), `${ux},${uy}: ${t}px still overlaps`);
|
||||
assert.ok(!apart(t - 0.5, ux, uy), `${ux},${uy}: ${t}px is further than needed`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("a chip on the ring", () => {
|
||||
test("with nothing measured it keeps the proportions the ring always used", () => {
|
||||
// The unit suite and the very first render both call it this way. 27/44 and
|
||||
// 25/42 of the seat's own reach, so the chips move with the ring rather than
|
||||
// with the felt.
|
||||
const seat = Render.seatPosition(3, 9, 1.4, RADII);
|
||||
const chip = Render.betPosition(3, 9, 1.4, RADII, null);
|
||||
assert.ok(Math.abs((chip.left - 50) - (seat.left - 50) * (27 / 44)) < 1e-9);
|
||||
assert.ok(Math.abs((chip.top - 50) - (seat.top - 50) * (25 / 42)) < 1e-9);
|
||||
});
|
||||
|
||||
test("measured, it sits just clear of its own puck rather than at a fixed fraction", () => {
|
||||
const box = felt(560, 400);
|
||||
const spacing = spacingOn(box);
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const seat = seatPixels(i, 9, box);
|
||||
const chip = chipPixels(i, 9, box, spacing);
|
||||
assert.ok(!boxesOverlap(seat, chip, PUCK, CHIP),
|
||||
`seat ${i + 1}'s own chip is under its own puck`);
|
||||
}
|
||||
});
|
||||
|
||||
test("and never wanders into the pot badge in the middle", () => {
|
||||
// The floor: 38% of the way out. Without it a chip walked in past a puck that
|
||||
// is 96px wide on a felt whose reach is 90px would land on the pot total, which
|
||||
// is the same fault at the other end of the spoke.
|
||||
const box = felt(300, 216);
|
||||
const spacing = spacingOn(box, { width: 200, height: 150 });
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const seat = seatPixels(i, 6, box);
|
||||
const chip = chipPixels(i, 6, box, spacing);
|
||||
const seatReach = Math.hypot(seat.x - box.width / 2, seat.y - box.height / 2);
|
||||
const chipReach = Math.hypot(chip.x - box.width / 2, chip.y - box.height / 2);
|
||||
assert.ok(chipReach >= seatReach * 0.38 - 0.5,
|
||||
`chip ${i + 1} is ${chipReach.toFixed(1)}px out of a possible ${seatReach.toFixed(1)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("a seat sitting dead centre gets its chip put somewhere, not NaN", () => {
|
||||
const box = felt(400, 300);
|
||||
const pos = Render.betPosition(0, 1, box.width / box.height, { rx: 0, ry: 0 }, spacingOn(box));
|
||||
assert.ok(Number.isFinite(pos.left) && Number.isFinite(pos.top), JSON.stringify(pos));
|
||||
});
|
||||
});
|
||||
|
||||
describe("deciding whether a ring of chips fits at all", () => {
|
||||
// The shape the layout works in: one entry per chip on the felt, carrying its own
|
||||
// measured size, because a chip is as wide as the number printed on it.
|
||||
const tableOf = (count, chip = CHIP) => {
|
||||
Render.state = { seatCount: count, seats: Array.from({ length: count }, () => ({})) };
|
||||
return Array.from({ length: count }, (_, i) =>
|
||||
({ index: i, width: chip.width, height: chip.height }));
|
||||
};
|
||||
|
||||
test("a desktop felt has room for one", () => {
|
||||
const box = felt(700, 500);
|
||||
const all = tableOf(9);
|
||||
assert.equal(Render.chipsClearTheSeats(all, box.width / box.height, RADII, spacingOn(box)), true);
|
||||
});
|
||||
|
||||
test("a phone on its side has not, whatever the window's width says about it", () => {
|
||||
// 692x117, measured on an 844px-wide phone in landscape — a viewport that sails
|
||||
// past a `max-width: 700px` test. The ring is a slot: the 38% floor puts a chip
|
||||
// on the seat across the table from it.
|
||||
const box = felt(692, 117);
|
||||
const all = tableOf(9);
|
||||
assert.equal(Render.chipsClearTheSeats(all, box.width / box.height, RADII, spacingOn(box)), false,
|
||||
"this is the felt where every chip was painted behind a puck");
|
||||
});
|
||||
|
||||
test("its answer is the one the geometry gives", () => {
|
||||
// Not a restatement of the two cases above: whatever it returns, it has to agree
|
||||
// with laying the chips out and measuring them against every puck on the felt.
|
||||
const shapes = [felt(700, 500), felt(692, 117), felt(300, 216), felt(1000, 620), felt(480, 300)];
|
||||
shapes.forEach(box => {
|
||||
const all = tableOf(9);
|
||||
const spacing = spacingOn(box);
|
||||
const aspect = box.width / box.height;
|
||||
const clear = all.every(({ index: i }) => {
|
||||
const chip = chipPixels(i, 9, box, spacing);
|
||||
return all.every(({ index: j }) => !boxesOverlap(chip, seatPixels(j, 9, box), CHIP, PUCK));
|
||||
});
|
||||
assert.equal(Render.chipsClearTheSeats(all, aspect, RADII, spacing), clear,
|
||||
`${box.width}x${box.height}`);
|
||||
});
|
||||
});
|
||||
|
||||
test("each chip is asked about at its own size", () => {
|
||||
// A chip is as wide as the number on it: at 768x1024, four players' chips were
|
||||
// 40px and one holding a four-figure bet was 47. Answering the question once, at
|
||||
// the widest, told the whole felt it had no room for a ring — and dropped all
|
||||
// five chips onto their own players' stacks to solve a problem four of them
|
||||
// did not have.
|
||||
// The felt and the pucks are the iPad's own, measured: 672x672, pucks 96x88.
|
||||
const box = felt(672, 672);
|
||||
const narrow = { width: 40, height: 20 };
|
||||
const huge = { width: 420, height: 380 };
|
||||
const spacing = { box, puck: { width: 96, height: 88 }, clearance: Render.BET_CLEARANCE_PX };
|
||||
const aspect = box.width / box.height;
|
||||
|
||||
assert.equal(Render.chipsClearTheSeats(tableOf(9, narrow), aspect, RADII, spacing), true,
|
||||
"a felt this size has room for a ring of ordinary chips");
|
||||
assert.equal(Render.chipsClearTheSeats(tableOf(9, huge), aspect, RADII, spacing), false,
|
||||
"and none at all for a ring of chips that size");
|
||||
|
||||
// Now one of each. The answer has to come from the sizes in the list, not from
|
||||
// one size standing in for all of them: with one huge chip the ring is off, and
|
||||
// taking that chip out of the question puts it back.
|
||||
const mixed = tableOf(9, narrow);
|
||||
mixed[0] = { index: 0, ...huge };
|
||||
assert.equal(Render.chipsClearTheSeats(mixed, aspect, RADII, spacing), false);
|
||||
assert.equal(Render.chipsClearTheSeats(mixed.slice(1), aspect, RADII, spacing), true,
|
||||
"the eight ordinary ones were never the problem");
|
||||
});
|
||||
|
||||
test("and at the chips against each other", () => {
|
||||
// The ring is a smaller circle than the seats' one and holds the same number of
|
||||
// things, so it is the tighter fit of the two. Measured on a Pixel 7 with twelve
|
||||
// players, two $10s on the ring sat on top of each other while every puck on the
|
||||
// felt was clear — so a check that only looks at the pucks says yes to a layout
|
||||
// that hides one player's bet behind another's.
|
||||
// The Pixel 7's own numbers, measured: a 300x336 felt on a ring of 44 by 40.84,
|
||||
// pucks 44x55, a pot badge 64x53, and the seven players who had put chips in.
|
||||
const box = felt(300, 336);
|
||||
const radii = { rx: 44, ry: 40.84 };
|
||||
const puck = { width: 44, height: 55 };
|
||||
const pot = { x: 150, y: 168, width: 64, height: 53 };
|
||||
const spacing = { box, puck, pot, clearance: Render.BET_CLEARANCE_PX };
|
||||
const aspect = box.width / box.height;
|
||||
|
||||
Render.state = { seatCount: 12, seats: Array.from({ length: 12 }, () => ({})) };
|
||||
const bettors = [2, 3, 4, 5, 6, 7, 8];
|
||||
const all = bettors.map(index => ({ index, width: index === 2 ? 44.1 : 49.7, height: 21 }));
|
||||
|
||||
const chips = all.map(entry => ({
|
||||
...entry,
|
||||
...chipPixels(entry.index, 12, box,
|
||||
{ ...spacing, chip: { width: entry.width, height: entry.height } }, radii),
|
||||
}));
|
||||
const seats = [...Array(12).keys()].map(i => seatPixels(i, 12, box, radii));
|
||||
|
||||
assert.equal(chips.some(chip => seats.some(seat => boxesOverlap(chip, seat, chip, puck))), false,
|
||||
"this table is chosen so the pucks are not the problem");
|
||||
assert.equal(chips.some(chip => boxesOverlap(chip, pot, chip, pot)), false,
|
||||
"nor the pot");
|
||||
assert.ok(chips.some((chip, i) => chips.some((other, j) => i !== j &&
|
||||
boxesOverlap(chip, other, chip, other))), "and so the chips are");
|
||||
|
||||
assert.equal(Render.chipsClearTheSeats(all, aspect, radii, spacing), false);
|
||||
});
|
||||
|
||||
test("it looks at every puck, not just the chip's own", () => {
|
||||
// The rule this replaced walked each chip in until it was clear of its *own*
|
||||
// seat and stopped there, which says nothing about the eight other pucks on the
|
||||
// felt. 560x190 is a felt where that rule is satisfied and the ring is still no
|
||||
// good: every chip clears the puck it belongs to, and one of them is sitting on
|
||||
// a stranger's.
|
||||
const box = felt(560, 190);
|
||||
const all = tableOf(9);
|
||||
const spacing = spacingOn(box);
|
||||
const chips = all.map(({ index: i }) => chipPixels(i, 9, box, spacing));
|
||||
|
||||
all.forEach(({ index: i }) => assert.ok(!boxesOverlap(chips[i], seatPixels(i, 9, box), CHIP, PUCK),
|
||||
`chip ${i + 1} is under its own puck, so this felt does not make the point`));
|
||||
assert.ok(all.some(({ index: i }) => all.some(({ index: j }) => i !== j &&
|
||||
boxesOverlap(chips[i], seatPixels(j, 9, box), CHIP, PUCK))),
|
||||
"expected a chip on somebody else's puck — what the old rule could not see");
|
||||
assert.equal(Render.chipsClearTheSeats(all, box.width / box.height, RADII, spacing), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a chip clinging to its puck", () => {
|
||||
// The fallback when there is no room for a ring: the chip hangs off the puck on the
|
||||
// side facing the empty middle of the felt.
|
||||
//
|
||||
// The felts the app actually draws, with the puck size each one settles on — read
|
||||
// off the running site at each viewport rather than guessed here, because the tight
|
||||
// felts shrink the puck too, and a test that holds the desktop's 96x64 puck against
|
||||
// a 528x83 felt is describing a table the app never draws.
|
||||
const REAL = [
|
||||
["Galaxy S8 on its side", felt(528, 83), { width: 44, height: 55 }],
|
||||
["iPhone on its side", felt(692, 113), { width: 72, height: 55 }],
|
||||
["Galaxy S8 upright, six-handed", felt(248, 280), { width: 78, height: 88 }],
|
||||
["Galaxy S8 upright, packed", felt(248, 280), { width: 44, height: 55 }],
|
||||
];
|
||||
// Chips are 21x17 everywhere they are anchored — the amount is the only thing on
|
||||
// one at that size.
|
||||
const TIGHT = { puck: { width: 44, height: 55 }, chip: { width: 21, height: 17 } };
|
||||
|
||||
// What each chip is told to do, worked out for the whole table at once.
|
||||
const placeAll = (count, box, puck, chip = TIGHT.chip, radii = RADII) => {
|
||||
Render.state = { seatCount: count, seats: Array.from({ length: count }, () => ({})) };
|
||||
const indices = [...Array(count).keys()];
|
||||
const entries = indices.map(i => ({ index: i, width: chip.width, height: chip.height }));
|
||||
const spacing = { box, puck, clearance: Render.BET_CLEARANCE_PX };
|
||||
const offs = Render.anchoredChipTravel(entries, count, box.width / box.height, RADII, spacing);
|
||||
return indices.map(i => {
|
||||
const seat = Render.seatPosition(i, count, box.width / box.height, radii);
|
||||
const at = { x: (seat.left / 100) * box.width, y: (seat.top / 100) * box.height };
|
||||
const off = offs[i];
|
||||
return { seat: at, chip: { x: at.x + off.x, y: at.y + off.y }, off, puck };
|
||||
});
|
||||
};
|
||||
|
||||
test("on its own, a chip asks to sit just clear of its puck", () => {
|
||||
const box = felt(692, 113);
|
||||
const puck = { width: 72, height: 55 };
|
||||
const seat = Render.seatPosition(0, 9, box.width / box.height, RADII);
|
||||
const off = Render.anchoredChipOffset(seat, puck, TIGHT.chip, box, RADII);
|
||||
const travel = Math.hypot(off.x, off.y);
|
||||
const ux = off.x / travel, uy = off.y / travel;
|
||||
assert.ok(Math.abs(travel -
|
||||
(Render.clearanceReach(puck, TIGHT.chip, ux, uy) + Render.BET_CLEARANCE_PX)) < 1e-6, `travelled ${travel}`);
|
||||
});
|
||||
|
||||
test("and gets it, where nothing else is in the way", () => {
|
||||
// 692x113 is the roomier of the two landscape felts. Every chip clears its own
|
||||
// puck and none of them has to give ground.
|
||||
placeAll(9, felt(692, 113), { width: 72, height: 55 }).forEach((row, i) => {
|
||||
assert.ok(!boxesOverlap(row.seat, row.chip, row.puck, TIGHT.chip),
|
||||
`seat ${i + 1}'s chip is inside its own puck, and nothing made it go there`);
|
||||
});
|
||||
});
|
||||
|
||||
test("it goes off the rim, not down the spoke", () => {
|
||||
// On a felt 692x113 the two are nowhere near the same direction. A seat on the
|
||||
// top rail has a spoke pointing diagonally inward — which along a slot this flat
|
||||
// runs sideways into the next seat — while off the rim is very nearly straight
|
||||
// down, into the empty middle. This is the seam that put two chips on top of
|
||||
// each other on a Galaxy S8.
|
||||
const box = felt(692, 113);
|
||||
const rows = placeAll(9, box, { width: 72, height: 55 });
|
||||
const top = rows.find(r => r.seat.y < box.height * 0.3 && r.seat.x > box.width * 0.7);
|
||||
assert.ok(top, "expected a seat up on the right of the top rail");
|
||||
assert.ok(Math.abs(top.off.y) > Math.abs(top.off.x) * 2,
|
||||
`chip went ${top.off.x.toFixed(1)},${top.off.y.toFixed(1)} — that is along the rail, not off it`);
|
||||
assert.ok(top.off.y > 0, "and downward, toward the middle of the felt");
|
||||
});
|
||||
|
||||
test("no two chips on a full table land on each other", () => {
|
||||
// Four sides gave three seams. There are no sides now, and this is what that is
|
||||
// worth — on every felt the app actually draws, at every table size.
|
||||
REAL.forEach(([name, box, puck]) => {
|
||||
[6, 9, 12].forEach(count => {
|
||||
const rows = placeAll(count, box, puck);
|
||||
for (let a = 0; a < rows.length; a++) {
|
||||
for (let b = a + 1; b < rows.length; b++) {
|
||||
assert.ok(!boxesOverlap(rows[a].chip, rows[b].chip, TIGHT.chip, TIGHT.chip),
|
||||
`${name}, ${count} seats: chips ${a + 1} and ${b + 1} overlap`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("a chip only gives ground when it is standing on something", () => {
|
||||
// The point of pulling chips back one at a time rather than by a rule that
|
||||
// applies to all of them. Stopping every chip at the felt's midline, or scaling
|
||||
// them all by one factor, takes travel from the seats at the ends of a long
|
||||
// rail — measured at 692x113, that parked two chips on their own player's stack
|
||||
// to solve a collision that did not exist.
|
||||
//
|
||||
// So: every chip either got everything it asked for, or putting the last step
|
||||
// back reintroduces a collision it is part of. The step goes back for all of
|
||||
// them at once, because a facing pair gives ground together and the pass that
|
||||
// finally separates such a pair moves both ends of it.
|
||||
const step = Render.CHIP_PULLBACK_STEP_PX;
|
||||
REAL.forEach(([name, box, puck]) => {
|
||||
[6, 9, 12].forEach(count => {
|
||||
const rows = placeAll(count, box, puck).map(row => {
|
||||
const travel = Math.hypot(row.off.x, row.off.y);
|
||||
const dir = travel < 1e-6 ? { x: 0, y: 0 } : { x: row.off.x / travel, y: row.off.y / travel };
|
||||
const asked = travel < 1e-6 ? 0 :
|
||||
Render.clearanceReach(puck, TIGHT.chip, dir.x, dir.y) +
|
||||
Render.BET_CLEARANCE_PX;
|
||||
const back = Math.min(asked, travel + step);
|
||||
return { ...row, travel, asked,
|
||||
undone: { x: row.seat.x + dir.x * back, y: row.seat.y + dir.y * back } };
|
||||
});
|
||||
|
||||
rows.forEach((row, i) => {
|
||||
if (row.travel < 1e-6 || row.travel >= row.asked - 1e-6) return;
|
||||
const onSomething = rows.some((other, j) => j !== i && (
|
||||
boxesOverlap(row.undone, other.undone, TIGHT.chip, TIGHT.chip) ||
|
||||
boxesOverlap(row.undone, other.seat, TIGHT.chip, puck)));
|
||||
assert.ok(onSomething,
|
||||
`${name}, ${count} seats: chip ${i + 1} stopped ${(row.asked - row.travel).toFixed(1)}px short of nothing`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("nobody is pushed further out than they asked to go", () => {
|
||||
// The pull-back only ever takes travel away. A chip pushed past its clearance
|
||||
// would be floating out on the felt with nothing to say whose it is.
|
||||
REAL.forEach(([name, box, puck]) => {
|
||||
placeAll(12, box, puck).forEach((row, i) => {
|
||||
const travel = Math.hypot(row.off.x, row.off.y);
|
||||
if (travel < 1e-6) return;
|
||||
const ux = row.off.x / travel, uy = row.off.y / travel;
|
||||
const asked = Render.clearanceReach(puck, TIGHT.chip, ux, uy) +
|
||||
Render.BET_CLEARANCE_PX;
|
||||
assert.ok(travel <= asked + 0.5,
|
||||
`${name}: chip ${i + 1} went ${travel.toFixed(1)}px when ${asked.toFixed(1)} was asked for`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("the direction turns smoothly all the way round the ring", () => {
|
||||
// The property a set of discrete sides cannot have, and the reason there are
|
||||
// none: neighbouring seats get neighbouring directions, with no step anywhere.
|
||||
const box = felt(560, 400);
|
||||
const angles = Array.from({ length: 24 }, (_, i) => {
|
||||
const seat = Render.seatPosition(i, 24, box.width / box.height, RADII);
|
||||
const off = Render.anchoredChipOffset(seat, PUCK, CHIP, box, RADII);
|
||||
return Math.atan2(off.y, off.x);
|
||||
});
|
||||
angles.forEach((angle, i) => {
|
||||
const next = angles[(i + 1) % angles.length];
|
||||
let turn = Math.abs(next - angle);
|
||||
if (turn > Math.PI) turn = 2 * Math.PI - turn;
|
||||
assert.ok(turn < Math.PI / 4,
|
||||
`between seats ${i + 1} and ${i + 2} the chip jumps ${(turn * 180 / Math.PI).toFixed(0)}°`);
|
||||
});
|
||||
});
|
||||
|
||||
test("it stops short of the pot total as well", () => {
|
||||
// The other thing in the middle of the felt, and the one number on the table
|
||||
// that belongs to everybody. A seat on the middle of a rail sends its chip
|
||||
// straight at it: on a Galaxy S8 on its side, seat 1's $10 was sitting on
|
||||
// "Pot $65". The badge is measured rather than assumed to be at the centre —
|
||||
// it lives on the table, not on either overlay, and the two are not concentric.
|
||||
const box = felt(528, 83);
|
||||
const pot = { x: 264, y: 40, width: 96, height: 34 };
|
||||
Render.state = { seatCount: 12, seats: Array.from({ length: 12 }, () => ({})) };
|
||||
const indices = [...Array(12).keys()];
|
||||
const entries = indices.map(i =>
|
||||
({ index: i, width: TIGHT.chip.width, height: TIGHT.chip.height }));
|
||||
const spacing = { box, puck: TIGHT.puck, clearance: Render.BET_CLEARANCE_PX, pot };
|
||||
const offs = Render.anchoredChipTravel(entries, 12, box.width / box.height, RADII, spacing);
|
||||
|
||||
indices.forEach(i => {
|
||||
const seat = Render.seatPosition(i, 12, box.width / box.height, RADII);
|
||||
const chip = { x: (seat.left / 100) * box.width + offs[i].x,
|
||||
y: (seat.top / 100) * box.height + offs[i].y };
|
||||
assert.ok(!boxesOverlap(chip, pot, TIGHT.chip, pot),
|
||||
`chip ${i + 1} is on the pot total`);
|
||||
});
|
||||
});
|
||||
|
||||
test("a seat with no rim to be off still gets an answer", () => {
|
||||
const box = felt(400, 300);
|
||||
const off = Render.anchoredChipOffset({ left: 50, top: 50 }, PUCK, CHIP, box, RADII);
|
||||
assert.ok(Number.isFinite(off.x) && Number.isFinite(off.y), JSON.stringify(off));
|
||||
assert.ok(off.y < 0, "upward, which is where a puck's own writing is not");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
const { test, describe } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { Betting, makeTable, chipsOnTable } = require("./load");
|
||||
|
||||
describe("Betting.beginHand", () => {
|
||||
test("blinds are posted and the seat after the big blind acts first", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], dealerSeatIndex: 0, smallBlind: 1, bigBlind: 2 });
|
||||
Betting.beginHand(state);
|
||||
|
||||
assert.equal(state.seats[1].committed, 1, "small blind");
|
||||
assert.equal(state.seats[2].committed, 2, "big blind");
|
||||
assert.equal(state.currentBet, 2);
|
||||
assert.equal(state.actingSeatIndex, 3, "under the gun");
|
||||
});
|
||||
|
||||
test("heads-up, the button posts the small blind and acts first pre-flop", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [2, 5], dealerSeatIndex: 2 });
|
||||
Betting.beginHand(state);
|
||||
|
||||
assert.equal(state.seats[2].committed, 1, "the button is the small blind");
|
||||
assert.equal(state.seats[5].committed, 2);
|
||||
assert.equal(state.actingSeatIndex, 2, "the small blind acts first pre-flop heads-up");
|
||||
});
|
||||
|
||||
test("heads-up, the big blind acts first once the flop is out", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [2, 5], dealerSeatIndex: 2 });
|
||||
Betting.beginHand(state);
|
||||
Betting.call(state, 2);
|
||||
Betting.check(state, 5);
|
||||
Betting.closeStreet(state);
|
||||
|
||||
assert.equal(state.street, "flop");
|
||||
assert.equal(state.actingSeatIndex, 5, "the big blind is first to act after the flop");
|
||||
});
|
||||
|
||||
test("a blind bigger than a stack puts that player all-in without stalling the action", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1], dealerSeatIndex: 0, smallBlind: 50, bigBlind: 100 });
|
||||
state.seats[0].stack = 10; // can't cover the small blind
|
||||
|
||||
Betting.beginHand(state);
|
||||
assert.equal(state.seats[0].stack, 0, "all-in for what they had");
|
||||
assert.equal(state.seats[0].committed, 10);
|
||||
assert.notEqual(state.actingSeatIndex, 0, "a player with no chips has nothing to decide");
|
||||
});
|
||||
|
||||
test("posting blinds never invents or destroys chips", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
||||
const before = chipsOnTable(state);
|
||||
Betting.beginHand(state);
|
||||
assert.equal(chipsOnTable(state), before);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Betting street completion", () => {
|
||||
test("a street ends once everyone has matched the bet and had a turn", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
|
||||
Betting.call(state, 3);
|
||||
Betting.call(state, 0);
|
||||
Betting.call(state, 1);
|
||||
assert.equal(Betting.isStreetComplete(state), false, "the big blind still has an option");
|
||||
|
||||
Betting.check(state, 2);
|
||||
assert.equal(Betting.isStreetComplete(state), true);
|
||||
assert.equal(state.actingSeatIndex, null);
|
||||
});
|
||||
|
||||
test("a raise reopens the action for everyone who had already called", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
|
||||
Betting.call(state, 3);
|
||||
Betting.call(state, 0);
|
||||
assert.equal(state.seats[3].hasActed, true);
|
||||
|
||||
Betting.raiseTo(state, 1, 10);
|
||||
assert.equal(state.seats[3].hasActed, false, "seat 3 owes a response again");
|
||||
assert.equal(state.seats[0].hasActed, false);
|
||||
assert.equal(state.currentBet, 10);
|
||||
assert.equal(Betting.isStreetComplete(state), false);
|
||||
});
|
||||
|
||||
test("the action skips a player who is all-in but still in the hand", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
state.seats[3].stack = 5;
|
||||
Betting.beginHand(state);
|
||||
|
||||
Betting.allIn(state, 3);
|
||||
assert.equal(state.seats[3].stack, 0);
|
||||
assert.notEqual(state.actingSeatIndex, 3);
|
||||
// They are still live and can still win the pot.
|
||||
assert.ok(Betting.liveSeats(state).some(s => s.index === 3));
|
||||
});
|
||||
|
||||
test("closing a street sweeps chips into hand totals and resets the bet", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
Betting.call(state, 3);
|
||||
Betting.call(state, 0);
|
||||
Betting.call(state, 1);
|
||||
Betting.check(state, 2);
|
||||
|
||||
const before = chipsOnTable(state);
|
||||
Betting.closeStreet(state);
|
||||
|
||||
assert.equal(state.currentBet, 0);
|
||||
assert.equal(state.street, "flop");
|
||||
assert.equal(chipsOnTable(state), before, "sweeping moves chips, it doesn't change the total");
|
||||
state.seats.forEach(p => assert.equal(p.committed, 0));
|
||||
assert.equal(Betting.handTotal(state.seats[2]), 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Betting all-in run-outs", () => {
|
||||
test("with everyone all-in there is nobody left to ask", () => {
|
||||
const state = makeTable({ seatCount: 3, seated: [0, 1, 2], stack: 20, dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
Betting.allIn(state, 2);
|
||||
Betting.allIn(state, 0);
|
||||
Betting.allIn(state, 1);
|
||||
|
||||
assert.equal(Betting.canStillBet(state), false);
|
||||
assert.equal(Betting.isRunningOut(state), true, "the board just gets dealt out");
|
||||
|
||||
Betting.closeStreet(state);
|
||||
assert.equal(state.actingSeatIndex, null, "no phantom action on the flop");
|
||||
});
|
||||
|
||||
test("one player with chips against an all-in cannot bet alone", () => {
|
||||
const state = makeTable({ seatCount: 3, seated: [0, 1, 2], dealerSeatIndex: 0 });
|
||||
state.seats[1].stack = 5;
|
||||
Betting.beginHand(state);
|
||||
Betting.fold(state, 2);
|
||||
Betting.allIn(state, 0);
|
||||
Betting.allIn(state, 1);
|
||||
|
||||
assert.equal(Betting.canStillBet(state), false);
|
||||
assert.equal(Betting.isHandOver(state), false, "two players are still live for the showdown");
|
||||
});
|
||||
|
||||
test("everyone folding to one player ends the hand", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
Betting.fold(state, 3);
|
||||
Betting.fold(state, 0);
|
||||
Betting.fold(state, 1);
|
||||
|
||||
assert.equal(Betting.isHandOver(state), true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Betting money handling", () => {
|
||||
test("a player can never put in more than they have", () => {
|
||||
const state = makeTable({ seatCount: 3, seated: [0, 1, 2], stack: 30, dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
const moved = Betting.putIn(state, 0, 500);
|
||||
assert.equal(moved, 30);
|
||||
assert.equal(state.seats[0].stack, 0);
|
||||
});
|
||||
|
||||
test("raise-to is what a dealer says out loud, not raise-by", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
Betting.raiseTo(state, 3, 10);
|
||||
assert.equal(state.seats[3].committed, 10, "their street total is 10, not 10 on top");
|
||||
assert.equal(state.seats[3].stack, 90);
|
||||
});
|
||||
|
||||
test("the minimum raise has to match the last one", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
assert.equal(Betting.minRaiseTo(state), 4, "a big blind of 2 makes the first raise 4");
|
||||
|
||||
Betting.raiseTo(state, 3, 10);
|
||||
assert.equal(Betting.minRaiseTo(state), 18, "an 8-chip raise means the next one is to 18");
|
||||
});
|
||||
|
||||
test("winnings land back in front of the player", () => {
|
||||
const state = makeTable({ seatCount: 3, seated: [0, 1, 2], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
Betting.creditWinnings(state, [{ id: state.seats[0].id, amount: 42.5 }]);
|
||||
assert.equal(state.seats[0].stack, 142.5, "the button posts nothing three-handed");
|
||||
});
|
||||
|
||||
test("fractional blinds don't drift into floating-point dust", () => {
|
||||
const state = makeTable({ seatCount: 3, seated: [0, 1, 2], stack: 10, smallBlind: 0.1, bigBlind: 0.2 });
|
||||
Betting.beginHand(state);
|
||||
Betting.call(state, 0); // button limps
|
||||
Betting.call(state, 1); // small blind completes
|
||||
Betting.check(state, 2); // big blind is happy
|
||||
Betting.closeStreet(state);
|
||||
|
||||
assert.equal(chipsOnTable(state), 30, "0.1 + 0.2 arithmetic didn't lose a chip");
|
||||
assert.equal(Betting.potSoFar(state), 0.6);
|
||||
state.seats.forEach(p => assert.equal(p.stack, 9.8));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
// The community cards, as a thing the table remembers.
|
||||
//
|
||||
// The board is the one part of a hand this tool never had a place for: hole cards go on
|
||||
// a seat, chips go in the middle, and the five cards everybody plays lived only on the
|
||||
// felt in front of the dealer. It is opt-in, because most nights nobody wants to enter
|
||||
// cards at all — and everything that reads it has to behave exactly as it always did
|
||||
// when it is off.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { Betting, HoldemState, makeTable } = require("./load");
|
||||
|
||||
const dealt = (over = {}) => {
|
||||
const state = { ...makeTable({ seatCount: 6, stack: 200, smallBlind: 1, bigBlind: 2 }), ...over };
|
||||
Betting.beginHand(state);
|
||||
return state;
|
||||
};
|
||||
|
||||
describe("a new table", () => {
|
||||
test("has five empty slots and is not tracking them", () => {
|
||||
const state = HoldemState.createDefault();
|
||||
assert.deepEqual(state.board, ["", "", "", "", ""]);
|
||||
assert.equal(state.trackBoard, false,
|
||||
"off by default: a dealer who never asked for this should not be asked for cards");
|
||||
});
|
||||
});
|
||||
|
||||
describe("how many cards are out", () => {
|
||||
test("none, three, four, five", () => {
|
||||
assert.equal(Betting.boardCardsFor("preflop"), 0);
|
||||
assert.equal(Betting.boardCardsFor("flop"), 3);
|
||||
assert.equal(Betting.boardCardsFor("turn"), 4);
|
||||
assert.equal(Betting.boardCardsFor("river"), 5);
|
||||
});
|
||||
|
||||
test("and nothing at all for a street that isn't one", () => {
|
||||
assert.equal(Betting.boardCardsFor("showdown"), 0);
|
||||
assert.equal(Betting.boardCardsFor(undefined), 0);
|
||||
});
|
||||
|
||||
test("it matches the streets the betting engine actually deals", () => {
|
||||
// If a street is ever added, this is the table that has to hear about it —
|
||||
// a street with no entry silently draws no cards.
|
||||
Betting.STREETS.forEach(street => {
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(Betting.BOARD_CARDS, street),
|
||||
`${street} has no card count`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearing it", () => {
|
||||
test("the next hand takes the board with it", () => {
|
||||
const state = dealt();
|
||||
state.board = ["AH", "KD", "9C", "4S", "2H"];
|
||||
HoldemState.startNextHand(state);
|
||||
assert.deepEqual(state.board, ["", "", "", "", ""]);
|
||||
});
|
||||
|
||||
test("so does a misdeal", () => {
|
||||
const state = dealt();
|
||||
state.board = ["AH", "KD", "9C", "", ""];
|
||||
Betting.takeHandBack(state);
|
||||
assert.deepEqual(state.board, ["", "", "", "", ""]);
|
||||
});
|
||||
|
||||
test("and so does dealing a hand, however the table got here", () => {
|
||||
// Dealing twice off the same button — after a misdeal, or a table set up and
|
||||
// then dealt again — would otherwise rank the last hand's board against this
|
||||
// hand's cards, and it would look exactly like a real answer.
|
||||
const state = dealt();
|
||||
state.board = ["AH", "KD", "9C", "4S", "2H"];
|
||||
Betting.beginHand(state);
|
||||
assert.deepEqual(state.board, ["", "", "", "", ""]);
|
||||
});
|
||||
|
||||
test("but turning the tracking off does not", () => {
|
||||
// A dealer who taps the toggle mid-hand can tap it back; wiping the flop on
|
||||
// the way past would cost them cards they had already entered.
|
||||
const state = dealt({ trackBoard: true });
|
||||
state.board = ["AH", "KD", "9C", "", ""];
|
||||
state.trackBoard = false;
|
||||
assert.deepEqual(state.board, ["AH", "KD", "9C", "", ""]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a board off a file", () => {
|
||||
const migrated = board => HoldemState.migrate({
|
||||
seats: [HoldemState.makePlayer()], board,
|
||||
}).board;
|
||||
|
||||
test("a table saved before boards existed gets an empty one, not tracked", () => {
|
||||
const parsed = HoldemState.migrate({ seats: [HoldemState.makePlayer()] });
|
||||
assert.deepEqual(parsed.board, ["", "", "", "", ""]);
|
||||
assert.equal(parsed.trackBoard, false);
|
||||
});
|
||||
|
||||
test("it is always five slots, however many arrived", () => {
|
||||
assert.equal(migrated(["AH"]).length, 5);
|
||||
assert.equal(migrated(["AH", "KD", "9C", "4S", "2H", "3D", "5S"]).length, 5);
|
||||
assert.equal(migrated("not an array").length, 5);
|
||||
assert.equal(migrated(null).length, 5);
|
||||
});
|
||||
|
||||
test("anything that isn't a card becomes an empty slot", () => {
|
||||
assert.deepEqual(migrated(["AH", "ZZ", 7, null, "10H"]), ["AH", "", "", "", ""]);
|
||||
});
|
||||
|
||||
test("and a real one keeps its place, upper-cased", () => {
|
||||
assert.deepEqual(migrated(["ah", " kd ", "9c", "", ""]), ["AH", "KD", "9C", "", ""]);
|
||||
});
|
||||
|
||||
test("tracking is only on if the file said so, in so many words", () => {
|
||||
const on = HoldemState.migrate({ seats: [HoldemState.makePlayer()], trackBoard: true });
|
||||
const truthy = HoldemState.migrate({ seats: [HoldemState.makePlayer()], trackBoard: "yes" });
|
||||
assert.equal(on.trackBoard, true);
|
||||
assert.equal(truthy.trackBoard, false, "a string is a file that has been edited, not a yes");
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user