Commit b0a73b6
authored
Freebucks: stop metered sessions eating bounty grants, and stop calling it a cash-out (#2942)
## What a user reported
> It says cashed out but I didn't even do it, there's no mean to do it
in my end. Also the wallet thing is not here anymore.
They were right on both counts. Their Earn page said **Cashed out 30**,
they had never pressed anything, and their wallet was empty.
## What was actually happening
Two defects, and the second was hiding the first.
**1. A metered session was charged twice, in two currencies.** A
Freebucks-metered session is already paid for — the meter takes its
price off the daily pool and wallet before the admit row is written. But
`consumeRewardGrantForAdmission` ran after every fresh admission without
looking at which pool granted it, so a metered account opening GLM 5.3
Flash paid **5 Freebucks to the meter and a bounty unit worth 15 to the
grant debit**.
The unit was **destroyed, not converted**. Only
`convertEarnedSessionsToFreebucks` writes a `grant_convert` credit, and
the admission path never calls it — so the earnings left
`freebuff_manual_session_grant` and arrived nowhere, with nothing in the
ledger to show for it.
The reporting user's two bounties, correlated to the millisecond:
| time (UTC) | admit | meter charge | grant debit |
|---|---|---|---|
| 09-04 09:02:46.9 | `z-ai/glm-5.3-flash`, pool `freebucks` | 5 @ :47.08
| 1.0 @ :47.12 |
| 09-04 22:02:13.7 | `z-ai/glm-5.3-flash`, pool `freebucks` | 5 @ :14.00
| 1.0 @ :14.12 |
**2. The Earn page reported the destruction as income.**
`getBountyBalance` derives `spent` from `consumed_units`, which a GLM
session decrements exactly as a cash-out does, and the tile rendered
that on the meter as *"Cashed out — already in your wallet"*. For money
nobody had credited. That mislabel is also why the first defect went
unnoticed: the page was reassuring people their earnings were safe.
For the record, at the time of the audit there were **zero
`grant_convert` credits in the ledger, fleet-wide, ever**.
`monthly_bonus` (215 rows) was the only credit reason that had ever been
written.
## Changes
- **Guard the grant debit** on `grantedPool === FREEBUCKS_ADMIT_POOL`.
The grant pools and the meter are alternatives, never both — a metered
account does not consult the pools for its entitlement and must not
consult them for a debit. Regression test asserts the meter still
charges **and** the grant does not; it fails without the guard.
- **The "Cashed out" tile now reads the ledger** (`cashedOutFreebucks`),
never `spent`. Sessions genuinely spent on GLM get their own line under
the tiles instead of being folded into the wallet figure.
- **Log the cash-out route's previews and failures.** For its first two
days the only line it could emit was the success line on POST, and there
were zero of those — a reading that means either "nobody pressed the
button" or "every press failed", with no way to tell them apart on a
route that moves users' earnings.
- **Remediation script**, idempotent and self-verifying: it re-checks
each event's admit row and meter charge in the database before paying,
so a mistranscribed row is skipped rather than credited.
- **`docs/freebuff-freebucks.md`** records the rule and why the
attribution has a retention-bounded horizon.
## Remediation (already applied)
**9 sessions, 6 accounts, all 2026-09-04, repaid at 15 Freebucks each =
135 total.** Written as `grant_convert` credits — precisely what should
have happened to those units — so the tiles now reconcile instead of
claiming a cash-out with no credit behind it. The reporting user's
wallet holds 30.
Attribution is bounded by log retention, not by choice: `consumed_units`
carries no timestamp, so the only way to tell a double charge from a
legitimate pre-meter GLM session is to correlate the `[FreeSession]
session debited against a reward grant` lines against the pool on the
admit row at that instant. Over the window covering every metered GLM
admission since the meter went live (first one 09-03 16:49 UTC): 38
landed on pool `glm` (correct), 4 pre-meter, 9 on `freebucks`.
## Still open, not in this PR
**1,081 users hold 1,834.3 spendable bounty units — 27,514 Freebucks —
and not one cash-out has ever completed.** This PR stops the units being
eaten and makes the page honest about it, but it does not explain the
zero. The new preview logging is what will distinguish "nobody presses
it" from "every press fails" within a day of deploy.
## Testing
- `web`: 463 pass in `src/server/free-session` (incl. the new regression
test, verified to fail without the fix)
- `freebuff/web`: 7674 pass
- typecheck clean on both1 parent b0ef46c commit b0a73b6
8 files changed
Lines changed: 716 additions & 9 deletions
File tree
- docs
- freebuff/web/src
- components/earn
- server/bounties
- scripts
- web/src
- app/api/freebuff/freebucks/convert
- server/free-session
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
263 | 263 | | |
264 | 264 | | |
265 | 265 | | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
266 | 347 | | |
267 | 348 | | |
268 | 349 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
4 | 8 | | |
5 | 9 | | |
6 | 10 | | |
| |||
17 | 21 | | |
18 | 22 | | |
19 | 23 | | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
20 | 27 | | |
21 | 28 | | |
22 | 29 | | |
| |||
126 | 133 | | |
127 | 134 | | |
128 | 135 | | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
129 | 151 | | |
130 | 152 | | |
131 | 153 | | |
| |||
162 | 184 | | |
163 | 185 | | |
164 | 186 | | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
165 | 195 | | |
166 | 196 | | |
167 | 197 | | |
168 | 198 | | |
169 | | - | |
| 199 | + | |
170 | 200 | | |
171 | 201 | | |
172 | 202 | | |
| |||
188 | 218 | | |
189 | 219 | | |
190 | 220 | | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
191 | 236 | | |
192 | 237 | | |
193 | 238 | | |
| |||
Lines changed: 15 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
161 | 161 | | |
162 | 162 | | |
163 | 163 | | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
164 | 179 | | |
165 | 180 | | |
166 | 181 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
3 | 5 | | |
4 | 6 | | |
5 | 7 | | |
| |||
20 | 22 | | |
21 | 23 | | |
22 | 24 | | |
23 | | - | |
| 25 | + | |
24 | 26 | | |
25 | 27 | | |
26 | 28 | | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
27 | 44 | | |
28 | 45 | | |
29 | | - | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
30 | 52 | | |
31 | 53 | | |
32 | 54 | | |
| |||
65 | 87 | | |
66 | 88 | | |
67 | 89 | | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
68 | 112 | | |
69 | 113 | | |
70 | 114 | | |
71 | 115 | | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
72 | 120 | | |
73 | 121 | | |
74 | 122 | | |
| |||
0 commit comments