# Путь денег — от гранта до settle (топикальный вход, строка 167) > **Статус: топикальный ВХОД, НЕ источник истины.** Док собирает денежную тему, размазанную по D-нотам, реестру дефектов платформы, глоссарию и бэклогу, в один маршрут чтения. При конфликте с чем угодно побеждает `docs/architecture/05-decisions-log.md` (правило чтения — диета D39.125: живой файл + слайсы). Каждое утверждение здесь снабжено указателем на первичный носитель; формулы — дословно оттуда. Словарь терминов (холд · прирост · аргумент потолка · baseline · settle · leftover-reserved · SpendBound) — `docs/glossary.md` §«Деньги платформы» (`glossary.md:50`). ## 1. Канон одним абзацем Деньги видимы с первого вызова: телеметрия, потолки, явное согласие на пере-оплату (CLAUDE.md, цель 5). Леджер без потолка запрещён — Р7: книга обязана нести хотя бы один из `book_usd`/`day_usd` (`backend/internal/config/book.go:250-251`, текст валидатора: «a ledger with no ceiling is forbidden, Р7»). Суммы на пользовательский экран НЕ выходят — запрет D39.84, подтверждён D39.100/К-8 («страница лимитов = СТАТУС, не суммы», D-лог `:196`) и держится в контракте (`14-api-contract/openapi.yaml:1152`: «No window, no `resets_at`, no sums»). Защита от перерасхода двойная и не зависит от событий: холд платформы делает кредит недоступным следующему прогону, а движок сам останавливается на потолке — «an overspend is impossible even while the platform is blind. The event stream is freshness only» (док-коммент `Hold`, `platform/internal/pgstore/credits.go:148-151`). ## 2. Деньги ДВИЖКА (зона `backend/`) - **Леджер SQLite.** Таблица `spend`, две фигуры: `committed_usd` (потрачено, с сырым ответом провайдера) и `reserved_usd` (зарезервировано под летящий вызов). `Reserve` перед каждым платным вызовом сверяет `SUM(committed_usd + reserved_usd)` книги и дня с потолками (`backend/internal/store/ledger.go:44,58,63`; Р7 — коммент `:12`); `Settle` конвертирует резервацию в committed и персистит ответ (`ledger.go:162,199-210`). Суммарно по книге — `SpentUSD` (`ledger.go:314-320`). - **Гейт потолков — ПЕР-ВЫЗОВНЫЙ, и был им до эмиттер-пака** (`Reserve` на каждый свежий attempt; прежняя формулировка «на границе юнита» была неверна по отношению к потолкам — опровергнута приёмкой D39.131). На границе юнита сидел РЕПЭЙР-суб-бюджет — ужесточён до пер-вызовного с ценой вызова (строка 135 закрыта D39.131); эскалационный кап хоп НЕ прицениваает — перелёт ≤1 хопа, задокументирован и запинен (диспозиция D39.131 п.2д, реопен — живой инцидент). - **leftover-reserved зануляется write-open.** `store.Open` (путь записи, каждый `translate`) выполняет `recoverReservations` (`backend/internal/store/store.go:88`, функция `:218`: `UPDATE spend SET reserved_usd = 0 WHERE reserved_usd > 0`) — файл владеется одним процессом, значит любой reserved на открытии принадлежит несеттлённому прогону. `OpenReadOnly` этого прохода намеренно НЕ делает (`store.go:95-104`) ⇒ **reserved из read-only `status` — всегда остаток мёртвого процесса** (несущий факт формулы PD-158, см. §3). - **`--max-units` — ОБЪЁМНЫЙ потолок прогона, ортогональный денежному** (D39.165 §1б, принят D39.170). Ограничивает не деньги, а РАБОТУ: не больше N выходных ЮНИТОВ (гранулярность `units_total` манифеста — та же, в которой платформа продаёт главы) будет ОПЛАЧЕНО этим прогоном; юниты, отданные за $0 (резюм, ре-пин), ретраи и эскалации внутри юнита потолок не тратят. Принимает только `translate`. **Остановка по объёму — ЗАВЕРШЕНИЕ (exit 0), не пауза:** словарь кодов выхода не расширялся, различение живёт в отчёте прогона и в логе; отчёт разводит ДОСТАВКУ и ПЕРЕ-ДЕЛКУ. Носитель — `backend/internal/pipeline/volume.go`. ⚠ Проводка в платформу ГЕЙЧЕНА (`PD-422`): единственный писатель признака движения банка — дверь правок, рост АВТО-банка от майнинга флага не ставит. - **`--ceiling-usd` — КНИЖНЫЙ потолок, не бюджет прогона.** Дословно из флага: «the book USD ceiling in force for THIS RUN ONLY — it OVERRIDES book.yaml `ceilings.book_usd` and is never written back. It caps the book's CUMULATIVE committed+reserved spend, not this run's increment» (`backend/cmd/tmctl/invocation.go:114`; ⚠-коммент `main.go:110`). Ратификация — D39.122 п.2(в): пересчёт «пользовательский прирост → абсолют» — обязанность ПЛАТФОРМЫ, вторая денежная ось не заводится. День-потолок флаг НЕ перекрывает (PD-157). Проводка внутри: `Ceilings.BookUSD` в `Reserve` и именование сработавшего потолка в ошибке — `backend/internal/pipeline/stagerun.go:479-480,505-511` (стоп = `errReserveCeiling`). - **Потолки — wiring, не семантика:** `Ceilings` намеренно исключены из `BriefHash` («Wiring fields (paths, ceilings, db) deliberately excluded», `book.go:262-265`, канон `:267+`) ⇒ смена потолка не двигает ни снапшот, ни ре-билл (D39.110 п.2б). - **`status --json` отдаёт фигуры платформе:** `committed_usd`, `reserved_usd`, `book_ceiling_usd`, `ceiling_pct` = 100·(committed+reserved)/book_ceiling (`backend/internal/pipeline/status.go:155-158`). Дневной фигуры в status нет (PD-157). - **Леджер = НИЖНЯЯ граница** (строка 78 бэклога, `docs/PROGRESS.md:41`): «2xx body decode failed (call IS billed)» — провайдер списал, попытка в `request_log` не попадает; живой замер D39.86 — 3 вызова из 14, неизвестность $0.015111 при леджере $0.114378; движок сеттлит оценку и печатает `estimated-cost rows: 3`. Канал виден, фикс — money-паком (дизайн GENERALITY_PHASE2 §5.6). ## 3. Деньги ПЛАТФОРМЫ по шагам (зона `platform/`) 1. **Грант.** `Grant` — строка леджера `kind='grant'`, идемпотентная по `(аккаунт, source, source_id)`: `on conflict (user_id, source, source_id) do nothing` (`platform/internal/pgstore/credits.go:38-49,357-362`). Фри-тир — тот же механизм в ТОЙ ЖЕ транзакции, что создание аккаунта (`pgstore/identity.go:86-88,153-155`; ⚠ **дефолт `SignupGrantMicroUSD` — НОЛЬ** (`platform/internal/config/config.go:262`), а не «5 × 1_000_000», как утверждала прежняя редакция: предложение «на бете дефолт в НОЛЬ» (D39.110 п.3) ИСПОЛНЕНО, `PD-104` в регистре стоит `fixed(P7)`. ⚠ Испр. аудитом доков 28.08 (D39.167) — ложная сумма стояла в ЕДИНСТВЕННОМ месте, куда идут за деньгами. 2. **Выбор потолка — шкала в ГЛАВАХ** (D39.110: от 1 до максимума, ноль не выбираем; потолок принадлежит ПРОГОНУ, в `book.yaml` не пишется). **Максимум = Balance КАК ЕСТЬ** (D39.115 п.2а; эррата в шапке D-лога `:4`): `Hold` — дебет в момент взятия, баланс уже не содержит открытых холдов, повторное вычитание вдвое укорачивает шкалу. Реализация: `pricing.Model.Scale` — `affordable = balance / PerChapter`, кламп по остатку глав, дефолт = верх шкалы (ратифицировано D39.123 п.2е; компромисс «весь баланс под одну книгу» принят осознанно) — `platform/internal/pricing/pricing.go:75-98`. 3. **Ставка $0.03/глава — конфиг-константа платформы:** `DefaultPerChapter = 30_000` micro-USD (`pricing.go:33`), переопределяется `PerChapterMicroUSD` (`config.go:90-93,194,229`). ⚠ **Провенанс ниже — ИСТОРИЯ, а не действующее обоснование (D39.165 §1, 28.08):** реальная цена главы замерена по движковым леджерам — средняя **$0.0115–$0.0190**, максимум одной главы $0.0375, то есть константа завышена в **1.6–2.6×**, и «купить 10 глав» отдаёт движку $0.30, покупающий ~16–26 глав («денежная ручка с подписью „главы"», `PD-410`). РЕШЕНО: цена — от ОБЪЁМА ИСХОДНИКА (носитель `chapters.units_total`, пишется интейком за $0), настоящий стоп — объёмный потолок В ДВИЖКЕ (см. §2). ⛔ Константу по сегодняшним числам НЕ калибровать (D39.165 ⛔г) — гейт строка 202. Исторический провенанс: exp08 v2 $0.0219/глава → ревизия D30.4 +15–25% → округление ВВЕРХ (низкая ставка опаснее высокой — прогон встанет на потолке посреди книги; `pricing.go:19-33`). Временная мера беты — честная оценка движковая (строка 166). 4. **Холд прироста ДО спавна.** `holdTx` пишет отрицательную строку `('hold', -amount)` и тем же знаком двигает кэш баланса (`credits.go:163-206,375-385`); нехватка — `ErrInsufficientCredit`. Холд + прогон + попытка + очередь — одна транзакция (`pgstore/runs.go:104` StartRun, `:338` рестарт; форма D39.123 п.2д). 5. **Аргумент движку: `bookCap = committed + прирост×ставка`, БЕЗ reserved — PD-158, дословно из кода: `return m.committed + increment`** (`platform/internal/runs/spawn.go:195-196`). Обе фигуры читаются ОДНИМ вызовом `status --json` перед стартом (`bookMeter`, `spawn.go:206-223`); отсутствие любой из них = отказ спавна («absent is not zero», PD-40). Почему без reserved: write-open движка занулит leftover до первой резервации, значит цифра из read-only status к моменту сравнения уже мертва; ратифицированная ранее формула с reserved переплачивала запасом ровно на leftover сверх холда — исполнено обеими формулами против гейта (D39.123 п.2б, поправка к D39.122(в)). Аргумент ≠ холд: он больше холда на committed (глоссарий `:50`). Фактически ушедшее значение хранится в `run_attempts.ceiling_arg_micro_usd` (PD-144, миграция 00011); потолок уходит argv-шаблоном `{{usd}}` (`config.go` `CeilingArg`, `spawn.go:132-150`); строка 145 закрыта D39.122. 6. **Baseline и списание.** `baseline = committed` книги на старте попытки (`platform/internal/runs/spawn.go:82`=`baseline, bookCap := m.committed`); списание попытки = разница счётчика с baseline; счётчик ниже baseline = подменённая БД, сеттлится в ноль с WARN — сами цифры в лог не идут (D39.84; `reconcile.go:308-316`). 7. ⚠ **Терминальный вердикт ОПЕРАТОРА — путь холда мимо settle (добавлено 22.08, P8-FIX/D39.154).** Прогон, чей движок не ответит уже никогда, закрывает человек: `tmplatformctl run abandon --run --reason <текст> [--release-hold]`. Команда ОТКАЗЫВАЕТ, если попытка ещё называет юнит или несёт базовую линию траты — над живым движком прогон не закрывают. Холд возвращается ЦЕЛИКОМ в обоих случаях (к abandon допускается только попытка, не дошедшая до движка, значит она ничего не потратила), а флаг решает лишь КОГДА — сразу или на ближайшем свипе. До этого пака такого пути не было вовсе: холд чужого аккаунта висел, пока кто-нибудь не залезет в БД руками. 8. **Settle по фигуре движка.** `reconcile.settle` (`platform/internal/runs/reconcile.go`, греп `func (s *Service) settle(` — ⚠ номера строк в файлы живой зоны не ставим, пока её сессия работает) читает committed из status; `credits.Settle` одной транзакцией закрывает резервацию, возвращает холд и списывает spent; **spent > холда — капится холдом** с явной пометкой «capped at the hold; the engine reported …» (`credits.go:214-234`), перерасход движка счётом не абсорбируется. 9. **SpendBound (PD-159).** Отложенный settle читает пожизненный счётчик КНИГИ и без границы оплатил бы работу прогона-преемника (замер: $2.10 за прогон ценой $0.10, и преемник платил снова). Граница = наименьший baseline попыток той же книги, стартовавших ПОЗЖЕ: `committed = min(счётчик сейчас, SpendBound)` (`reconcile.go:293-307`; `pgstore/runs.go:268`). 10. **Открытое — эскроу/intent (строка 136, `docs/PROGRESS.md:85`):** write-ahead intent + состояние `uncertain`-эскроу + `closing`; холд НЕ освобождается по выходу юнита (сигнал обгоняет денежные события); spend не постится без свидетельства провайдера; расход сверх холда — полной суммой, превышение явной строкой убытка. Стройка по research/25, «скоро». До эмиттера (строка 103) платформа между спавном и settle слепа — защита держится холдом и потолком движка (§1). ## 4. Фронт (зона `frontend/`) Максимум шкалы — `Balance` КАК ЕСТЬ (D39.115 п.2а); `Reserved` — справочная величина, `ReadAccount` отдаёт обе раздельно (`pgstore/credits.go:99`). Шкала — в ГЛАВАХ: `CeilingBounds` — «in CHAPTERS. The chapters → money conversion lives on the platform and is not exposed here in any form»; клиенту запрещено клампить повторно; `max_chapters: 0` = состояние «исчерпано» вместо шкалы (`openapi.yaml:1116-1147`). Суммы на экран не идут: `Usage` = `state` (ok/low/exhausted) + `remaining_percent`, без окон и сумм (`openapi.yaml:1149-1165`); продуктовый носитель — ПТ-35 (`docs/product-requirements.md:62`): стоп по потолку = `paused` + оповещение «перевод остановлен: лимиты исчерпаны»; остаток — страница лимитов (S4+), слово владельца В-3, К-13 (`paused_reason` не различает «свой потолок» и «нет кредита»). ## 5. Открытые дыры и их носители | Дыра | Носитель | |---|---| | ~~PD-113 (major)~~ **ЗАКРЫТ P6/D39.132** (испр. оркестратором 15.08): обе половины построены — событие `ceiling{scope}` + exit 4 (движок, D39.131) и потребитель (платформа, П-15): потолочный стоп = `paused` двумя независимыми каналами, причина пишется одним оператором со статусом и перечитывается после drain. ⚠ **Хвост «открытых major в регистре платформы 0» СНЯТ 22.08 — он протух:** ⚠ **прежняя редакция называла открытым мажором `PD-370` — он ЗАКРЫТ** (обе половины: зонная 22.08, контрактная минором 0.5.0 D39.161; регистр переведён 28.08 аудитом доков D39.167), и держать счёт чужого регистра здесь — лишний носитель, который протухает молча; счёт печатает `python3 docs/scripts/counts.py` | `platform/docs/DEFECT_REGISTER.md` (эра P6), D39.132 | | ~~Строка 165~~ **ЗАКРЫТА D39.131** (испр. оркестратором 14.08): exit 4 = потолок · 5 = graceful stop · 10–19 полоса отказов; потребительская половина построена P6 (D39.132) — шов закрыт с обеих сторон; ⚠ exit 13 (`schema_mismatch`) едет паком migrate, ратификация направления — D39.132 | D39.131, D39.132 | | **Строка 166** — движковая поверхность оценки $/глава; $0.03 — временная мера беты (П-10 зоны) | `docs/PROGRESS.md:114` | | ~~**PD-104**~~ **ЗАКРЫТА** (`fixed(P7)`; дефолт гранта в коде — ноль, испр. 28.08 D39.167) — фри-тир: док↔код закрыто, продуктовая половина (ноль на бете · агрегатный потолок · счётчик) на владельце | `DEFECT_REGISTER.md:115`, D39.110 п.3 | | **Строка 78** — леджер = нижняя граница (биллящийся decode-fail мимо request_log, ≤13% захода на ре-пробе) | `docs/PROGRESS.md:41` | | **Строка 136** — эскроу/intent (см. §3 п.10) | `docs/PROGRESS.md:85` | | *(добавка сборщика)* **PD-157 (minor)** — `day_usd` книги флагом не перекрывается, платформой не виден и не задаётся; в `status --json` дневной фигуры нет | `DEFECT_REGISTER.md:168` | ## 6. Карта носителей темы | Вопрос | Где живёт | |---|---| | Словарь терминов | `docs/glossary.md:50` §«Деньги платформы» | | Управляемый потолок: решение и форма | D39.110 (+поправка D39.112 п.3: п.1 — пересказ, не цитата) | | Максимум шкалы = Balance как есть | D39.115 п.2а; эррата — шапка D-лога `:4` | | Семантика `--ceiling-usd` | D39.122 п.2в; `tmctl/invocation.go:114`, `main.go:110` | | Формула аргумента потолка (без reserved) · ставка · settle-формы | D39.123 п.2б/г/д; PD-158/PD-159/PD-144 в реестре | | Код денег платформы | `platform/internal/pgstore/credits.go` · `pgstore/runs.go` · `runs/spawn.go` · `runs/reconcile.go` · `pricing/pricing.go` | | Код денег движка | `backend/internal/store/ledger.go` · `store/store.go` · `pipeline/stagerun.go` · `pipeline/status.go` · `config/book.go` | | Дефекты денег | `platform/docs/DEFECT_REGISTER.md` (PD-104·113·144·157·158·159) | | Бэклог-строки | `docs/PROGRESS.md` строки 78 · 136 · 137 · 166 (135 и 165 закрыты D39.131) | | Контракт наружу | `14-api-contract/openapi.yaml` (`CeilingBounds`, `Usage`); ПТ-35 | | Запрет денег на экране | D39.84; D39.100/К-8; ПТ-35 |