Onboarding Economics — collect at setup, consume governed (2026-08-21)
Directive: owner, 2026-08-21 — "These costs must be collected during the
customer onboarding process along with API and other connectors" (extending
owner ruling 2026-08-20 §2, which produced the tenant setup surface).
Branch: claude/onboarding-economics-v1. Status of everything here:
implemented (code + tests on the branch); nothing below is deployed or
live-verified, and each consumer's arming posture is unchanged.
What already existed (measured before building)
The 2026-08-20 §2 build landed 2026-08-21 on main: /onboarding UI page →
BFF tenant-costs routes → gateway tenant_costs.py, storing the net-yield
cost block in the tenant vault (mizoki-connector-{tenant}-cost_config),
vocabulary parity-pinned, offboarding-covered. Two gaps remained:
- Nothing consumed what onboarding collected.
services/net-yieldread onlyconfig/net_yield_costs.yaml(the all-null template — the "5 NULLs" operator item), so the surface could report complete while compute still priced nothing. Measured: zero vault references inservices/net-yield/*.pybefore this branch. - The other declared economics were not collectable. Treasury (F5), the F2 LTV discount factor, and F3 inventory policy had no onboarding surface at all — they remained "owner supplies config" items.
What this branch adds
1. Net-yield vault read leg (services/net-yield/vault_costs.py)
- Secret-id formula duplicated byte-compatibly (the
execution_adapters/credentials.pyprecedent) and guarded bytests/connectors/test_vault_costs_parity.py— both images must agree or the suite fails. - Transport is the scoped
google-cloud-secret-managerSDK client — thebq.pysanctioned pattern. ADR-NY-001's no-HTTP-client property holds unchanged;test_flags.pystill passes with zero modifications. - Ships dark:
NET_YIELD_COSTS_VAULTunset → behavior byte-identical to the file path (/healthreportscost_source_vault: not_configured). Arming is a reviewed env change on the dispatch-only deploy (RUNBOOK §0b, added). - Resolution rule: vault document → serves through the same
parse_cost_configvalidator; absent → file path unchanged; the same tenant asserting non-null values in BOTH sources → 503cost_config_conflict(never a silent preference); vault read failures → named 503s (never silently-empty costs). TTL-cached (default 300 s). - Tests:
services/net-yield/test_vault_costs.py(13) + parity (4); full service suite 116 passed.
2. Declared-economics collection (tenant_economics.py, gateway)
New vault provider economics_declared on the same surface, one section per
consumer, each validated in that consumer's own vocabulary (parity-pinned by
path-import in test_tenant_economics.py):
| Section | Collectable keys | Deliberately excluded (named in tests) |
|---|---|---|
treasury (F5) |
liquidity_floor_usd, declared_cash_position_usd, position_as_of, base_proposal_cap_usd, covenant_max_proposal_usd | max_position_age_days, tightening_curve (policy shape) |
ltv (F2) |
quarterly_discount_factor | min_observed_quarters, horizon_quarters (governance floors) |
inventory (F3) |
skus{safety_stock_units, overstock_units, holding_cost_per_unit_day}, defaults, freshness | fulfillment_nodes, triggers (routing policy) |
Rules enforced at entry: refuse-never-guess (unknown keys/types/negatives
422); a declared cash position requires its as-of date (the F5 staleness
rule); discount factor in (0, 1]; safety stock below overstock; freshness
positive; roster bounded (500). Gap names follow each consumer's own
vocabulary; the treasury gap rule mirrors TenantTreasury.usable() and the
pin exercises the consumer's dataclass directly. Values are never logged.
NON_CATALOG_VAULT_PROVIDERS gains the provider, so tenant-wide erasure
(destroy_tenant_vault) covers it. Tests: 21.
COLLECTED is not ARMED. No consumer read path changed for F5/F2/F3: the
governance trio's deploy verify step still asserts treasury
not_configured, and each lane arms via its own reviewed config step. This
store is the merchant-declared source of record those steps install from.
3. UI + BFF (/onboarding page)
lib/bff/adapters/tenant-economics.ts,app/api/bff/tenant-economics/(GET + save, tenant fail-closed on reads AND writes — the tenant comes only from the middleware-stamped header).- The onboarding page gains a "Declared economics" card (treasury / LTV / inventory sections, same form idiom, client-side pre-validation mirroring the server rules, honest collected-is-not-armed copy).
- Gates:
tsc --noEmit0 errors, lint 0 errors, vitest 972 passed (5 new).
Scope exclusions (named, with reasons)
- COGS worksheet upload route — the landed design defers per-variant
worksheet columns to a
cost_configschema bump (tenant_costs.py docstring); not re-opened here. The worksheet CLI path stands. - F1 creative / F4 geos — pilot-validation evidence and geo policy are owner/operator declarations, not merchant costs.
- Consumer arming for F5/F2/F3 — governance-gated by design; unchanged.
Deploy surfaces on merge (per rule 04)
| Service | Deploy path | Effect on merge |
|---|---|---|
| service-marketing-connectors | merge-fired CI | new routes serve (verify_caller-gated; collection inert until called) |
| miz-oki-command-center-ui | merge-fired CI | onboarding card serves (BFF fail-closed) |
| net-yield | dispatch-only (ADR-NY-001) | read leg merges but serves only on human dispatch — and stays dark even then until NET_YIELD_COSTS_VAULT is set (double-dark) |
Operator tail (in order, when the owner chooses to arm)
- Merchant completes
/onboarding(costs + declared economics) — collection is live as soon as the gateway/UI deploys land. - Net-yield: RUNBOOK §0b — vault
secretAccessorgrant → env flag via the reviewed dispatch deploy →/healthposture → one order end-to-end. - F5/F2/F3: each lane's existing reviewed arming step, installing the
declared values from
GET /api/v1/tenant/economics.