Money & billing · TrustedRouter Field Manual
Month 1

Money & billing

Credits, reserve/settle/refund, async settlement, and payment rails.

Video 09 Money & billing · Microdollars, the typed ledger, the reservation state machine, payment rails.
Video 02 Life of a request · Auth, privacy routing, reserve, stream, settle, analytics.

Research snapshot of quill-router at main @ 778120494 (2026-10-19). Read-only investigation; no production systems touched. Source paths are relative to the repo root; src/trusted_router/ is abbreviated tr/ in tables.

AGENTS.md: "reserve / settle / refund and anything touching credits are the highest-risk code in the repo. Changes there need a differential or conformance test, never review-by-eye."


1. Overview (the 5-minute version)

Unit of account. All money is integer microdollars (1 USD = 1,000,000 µ$). See tr/money.py. Floats exist only for response compatibility (microdollars_to_float). Token cost is (tokens * rate_micro_per_million + 500_000) // 1_000_000 (round half-up), tr/money.py:57.

Customer price. Upstream provider cost × 1.055 (STANDARD_PRICE_MARKUP_BASIS_POINTS = 550, tr/pricing.py:126), ceiling-rounded, with a floor of $0.01 per million tokens. Prices come from a committed snapshot (tr/data/openrouter_snapshot.json) that an hourly price-refresh bot (.github/workflows/refresh-prices.yml, scripts/pricing/refresh.py) regenerates and auto-commits to main, which auto-deploys. Big price jumps are held unless they appear in tr/pricing_approvals.py.

The ledger ("typed book"). Since 2026-06-26 the only authoritative money book is three native Spanner tables. The JSON-blob book in tr_entities was retired (docs/design/retire-json-credit-ledger.md, billing-dual-system-deletion-handoff.md):

Table Key Money columns
tr_credit_balance (workspace_id, shard) total_credits, total_usage, reserved, in_debt, billing_pause_causes
tr_key_limit (key_hash, shard) limit_micro (NULL = uncapped), usage, byok_usage, reserved, include_byok
tr_reservation reservation_id credit_reserved_micro, key_reserved_micro, actual_micro, settled, expires_at, terminal_at, idempotency scope

Available balance on a shard = total_credits - total_usage - reserved. A hot workspace can be split across N shards. Each shard is an independent sub-budget, so the global cap holds by construction (docs/design/credit-row-sharding-handoff.md). The JSON credit entity is now metadata only: auto-refill config and Stripe customer/payment-method ids.

Per-request lifecycle. The attested enclave (quill-cloud-proxy) calls the control plane's internal routes: 1. POST /internal/gateway/authorize → authorize_atomic reserves an estimate with one conditional DML per row: UPDATE ... SET reserved = reserved + @est WHERE headroom >= @est AND NOT in_debt. Row-count 1 means accepted, 0 means 402. The reservation row is inserted with expires_at. 2. Optional POST /internal/gateway/heartbeat records started_at plus the usage delivered so far. The reaper can use this later. 3. POST /internal/gateway/settle (success) or /refund (failure) → _settle_gateway_authorization prices the actual usage, freezes the amount into a SettleOutboxRow, then finalizes: - claim the reservation (UPDATE tr_reservation SET settled=true ... WHERE settled=false; first writer wins), - mark the authorization settled, write generation/activity, - release the exact holds and book actual (credit row first, then key row). 4. Reaper (reap_expired_reservations_result) reclaims holds that were never settled. It releases them free, or with Decision 70 snapshot booking it books the last heartbeat usage snapshot. It never frees a hold that a pending/dead settle-outbox row still intends to charge. 5. Settle-outbox drain (/internal/gateway/settle-outbox/drain, every 5 min) replays any intent whose inline finalize didn't land. It applies the frozen amount through a narrow primitive.

Why it's this complicated. Two failure directions: - Double charge. Prevented by the first-writer-wins reservation claim. N replays book at most once. - Lost charge. The request was served but the settle never landed, and the reaper released it free. Prevented by the durable settle outbox plus the reaper guard. Per the design, the reaper guard is the sole correctness mechanism for this direction. Drain timing only buys latency.

Async settlement (in flight, dormant). This is a multi-month program to move finalize off the response path. The enclave gets a 202 after one durable intent commit, and the drain books the charge. It is gated behind an enormous evidence apparatus: "shadow v2", "gates", "packs", "certificates", and a "frozen-main differential" proof_oracle. These prove that the change alters no money behavior before anyone flips it on. Production today: TR_ASYNC_SETTLE_ENABLED=false, TR_ASYNC_SETTLE_PROTECTION=false, TR_ASYNC_SETTLE_SHADOW_POLICY=shadow-v2 for one dedicated probe workspace (scripts/deploy/rollout.sh:659-664).

Money in. Payment rails (Stripe card/ACH/auto-refill, x402 stablecoin via Stripe, PayPal, Adyen (dark), Lightning (separate LightningRouter workers)) all funnel into one function: credit_workspace_typed_direct (tr/storage_gcp.py:3183). It is idempotent on a stripe_event marker entity keyed by a rail-specific event id, and it writes a trust "payment fact" in the same transaction.

Money out. Creator earnings from app markup and custom-model markup go to tr_earnings_balance and are paid via Routable ACH (dark, TR_ROUTABLE_ENABLED=false).


2. Invariants (memorize these)

# Invariant Where enforced
I1 All money is int microdollars. No floats in ledger math. Rounding is half-up per component (token cost) or ceiling (markup). tr/money.py, tr/pricing.py:134-170, tr/billing_snapshot.py (int64 overflow is an explicit error)
I2 Per shard: total_usage + reserved <= total_credits at reserve time. The reserve is a single conditional UPDATE that takes the write lock directly, with no read→upgrade. This is the fix for the wound-wait deadlock. reserve_credit tr/storage_gcp_counter_dml.py:68; design docs/design/billing-typed-counters.md §2
I3 Shards partition the workspace budget, so global overspend at admission is impossible. Sum over shards of credits/usage/reserved gives the workspace totals. docs/design/credit-row-sharding-handoff.md; tr/storage_gcp_credit_shards.py
I4 First writer wins on tr_reservation.settled false→true. Only the claim winner touches counters. Replays are 0-row no-ops. claim_reservation tr/storage_gcp_counter_dml.py:781
I5 Release subtracts the exact recorded hold (reserved >= @hold guard). Row-count must be 1. A 0-row release must never be silently accepted, because it strands the hold and loses the charge. release_credit tr/storage_gcp_counter_dml.py:299
I6 Lock order: credit row before key row, everywhere (authorize, finalize, reaper, top-up). No cycle. settle_atomic docstring tr/storage_gcp_authorize.py:958; tests/test_settle_lock_order.py
I7 A settle can exceed its hold (actual > estimate). The overrun is booked anyway (test_settle_books_usage_always.py). If a shard goes negative it is covered from other shards, or every shard is marked in_debt while the signed sum is negative. Marked rows refuse reservations. Inflows repay negative rows first, lowest shard first. tr/credit_debt.py, tr/storage_gcp_credit_debt.py:177; TLA+ proofs/CreditDebt.tla
I8 Reaper guard. The reaper never free-releases a reservation whose authorization has a tr_settle_outbox row in pending or dead. This is re-checked inside the read-write claim transaction. A scan-time filter alone is insufficient (MF2). Only a human-set release_approved re-permits a free release. tr/storage_gcp_authorize.py:1397-1440, _CLAIM_RESERVATION_GUARDED_SQL tr/storage_gcp_counter_dml.py:41
I9 Frozen amount wins (MF5). The drain books actual_cost_micro exactly as stored. It never reprices, even across deploys, catalog changes, or endpoint retirement. It also routes on the stored settle_origin (MF4). apply_frozen_settle tr/services/settle_outbox_apply.py:136
I10 Outbox PK is (authorization_id, intent_kind ∈ {settle, refund}). Enqueue is INSERT-as-claim (ALREADY_EXISTS), not an upsert. SpannerSettleOutbox.enqueue tr/storage_gcp_settle_outbox.py:500
I11 finalized=False is ambiguous. already_released_free on a row that intended a charge is an invariant violation → dead + alert, never "recovered". ApplyOutcome tr/services/settle_outbox_apply.py:78; design §3
I12 Analytics never destabilizes money. The benchmark analytics-outbox INSERT rides in the money commit only on the one-commit happy path. Every failure falls back to the two-commit settle, with the enqueue after commit and best-effort. tr/storage_gcp_analytics_outbox.py docstring; AGENTS.md "Money code"
I13 Payment credit is exactly-once per external payment. The stripe_event marker plus the trust payment fact (keyed by provider + PaymentIntent/capture id) are written in the same transaction as the total_credits bump. credit_workspace_typed_direct tr/storage_gcp.py:3183
I14 Refunds and chargebacks never silently debit customer credits. They become adverse trust events with recovery debt ("unrecovered principal"), absorbed from future inflows/releases, or an inbox entry for review. record_adverse_trust_event tr/storage_gcp.py:3324; absorb_unrecovered_recovery_tx tr/storage_gcp_trust.py:282
I15 Flag-off byte identity. Every new money mechanism ships dormant. With the flag off, SQL, parameters, and RPC sequence must be byte-identical to before. This is proven by the frozen-main differential. claim_reservation_statement docstring; tests/test_async_settle_proof_oracle.py
I16 Async acceptance commits responsibility, never money. The async enqueue transaction must not claim, book, release, or touch credit/key rows. Its reservation predicate (settled=false, row-count exactly 1) prevents acknowledging an unbookable charge. tr/storage_gcp_async_settle.py:28; docs/design/async-settle-outbox-v1.md §4
I17 Standing auditor tripwire: typed.reserved == Σ open typed holds (both directions, shard-aware). It runs daily in CI. audit_typed_invariants tr/storage_gcp_counter_reconcile.py:242; .github/workflows/typed-audit.yml

3. Reservation state machine

stateDiagram-v2
    [*] --> Held: authorize_atomic - conditional reserve OK
    [*] --> Rejected: row-count 0 (insufficient / in_debt / paused)
    Rejected --> [*]

    Held --> Held: heartbeat (started_at, delivered usage)
    Held --> IntentDurable: settle/refund - outbox INSERT (two-commit path)
    Held --> SettledCharged: one-commit settle (claim + book + intent done)
    Held --> Refunded: refund claim wins (actual=0)
    Held --> ReapedFree: reaper - expired, no pending/dead intent
    Held --> ReapedSnapshot: reaper - expired, started + snapshot booking

    IntentDurable --> SettledCharged: inline finalize claims reservation
    IntentDurable --> SettledCharged: drain apply_frozen_settle (frozen amount)
    IntentDurable --> Frozen: drain gives up (dead) - reaper blocked
    Frozen --> SettledCharged: human applies manual charge
    Frozen --> ReapedFree: human sets release_approved

    SettledCharged --> [*]: terminal_at armed (30-day retention)
    Refunded --> [*]
    ReapedFree --> [*]
    ReapedSnapshot --> [*]
Diagram source
stateDiagram-v2
    [*] --> Held: authorize_atomic - conditional reserve OK
    [*] --> Rejected: row-count 0 (insufficient / in_debt / paused)
    Rejected --> [*]

    Held --> Held: heartbeat (started_at, delivered usage)
    Held --> IntentDurable: settle/refund - outbox INSERT (two-commit path)
    Held --> SettledCharged: one-commit settle (claim + book + intent done)
    Held --> Refunded: refund claim wins (actual=0)
    Held --> ReapedFree: reaper - expired, no pending/dead intent
    Held --> ReapedSnapshot: reaper - expired, started + snapshot booking

    IntentDurable --> SettledCharged: inline finalize claims reservation
    IntentDurable --> SettledCharged: drain apply_frozen_settle (frozen amount)
    IntentDurable --> Frozen: drain gives up (dead) - reaper blocked
    Frozen --> SettledCharged: human applies manual charge
    Frozen --> ReapedFree: human sets release_approved

    SettledCharged --> [*]: terminal_at armed (30-day retention)
    Refunded --> [*]
    ReapedFree --> [*]
    ReapedSnapshot --> [*]

Notes: - "Claim" is the settled false→true transition. Every arrow into a terminal state goes through claim_reservation, so concurrent inline, drain, and reaper attempts resolve to exactly one winner. - A settle intent and a refund intent can coexist (different PKs). The first reservation claimant wins. The loser drains to already_settled_with_charge, plus a low-priority review flag if a refund lost to a charge. - Fast-admission pilot (TR_FASTPATH_PILOT_ENABLED=true for two workspaces): the hold lives in a lease on the Go "front door" service, not in tr_reservation. The reservation row "holds nothing". Settlement goes through _settle_leased_authorization / tr/services/fastpath_settle.py. TLA+ specs LeaseLifecycle, TerminalOrder, AuditorCommit, KeyCapFence are planned. CreditDebt and AutoRefillHandoff are implemented.


4. Settle sequence: one-commit happy path vs two-commit fallback

sequenceDiagram
    participant E as Enclave (quill-cloud-proxy)
    participant R as Router /internal/gateway/settle
    participant S as Spanner
    participant D as Outbox drain (5 min)

    E->>R: POST settle (authorization_id, actual usage)
    R->>S: read gateway authorization
    R->>R: price usage, build frozen SettleOutboxRow

    alt one-commit happy path (typed, outbox available)
        R->>S: read reservation
        R->>S: Batch DML: claim + auth finalize + INSERT intent(status=done) + activity + benchmark + credit release + key release
        S-->>R: commit OK
        R-->>E: 200 settled
    else any deviation: declined, abort, 5s budget, unknown outcome
        R->>S: rollback
        R->>S: commit 1: INSERT intent(status=pending, next_attempt +60s)
        Note over R,S: intent durable here, ALREADY_EXISTS finds a done row if the one-commit actually landed
        R->>S: commit 2: typed_finalize_atomic (claim, book, mark done in same txn)
        alt finalize OK
            S-->>R: settled
            R-->>E: 200 settled
        else finalize fails / crash
            R-->>E: intent_durable (or 5xx)
            D->>S: claim due rows (sharded sparse index, lease 300s)
            D->>S: apply_frozen_settle: claim + book frozen actual_cost_micro
            D->>S: mark done / dead / park (lease-fenced)
        end
    end
Diagram source
sequenceDiagram
    participant E as Enclave (quill-cloud-proxy)
    participant R as Router /internal/gateway/settle
    participant S as Spanner
    participant D as Outbox drain (5 min)

    E->>R: POST settle (authorization_id, actual usage)
    R->>S: read gateway authorization
    R->>R: price usage, build frozen SettleOutboxRow

    alt one-commit happy path (typed, outbox available)
        R->>S: read reservation
        R->>S: Batch DML: claim + auth finalize + INSERT intent(status=done) + activity + benchmark + credit release + key release
        S-->>R: commit OK
        R-->>E: 200 settled
    else any deviation: declined, abort, 5s budget, unknown outcome
        R->>S: rollback
        R->>S: commit 1: INSERT intent(status=pending, next_attempt +60s)
        Note over R,S: intent durable here, ALREADY_EXISTS finds a done row if the one-commit actually landed
        R->>S: commit 2: typed_finalize_atomic (claim, book, mark done in same txn)
        alt finalize OK
            S-->>R: settled
            R-->>E: 200 settled
        else finalize fails / crash
            R-->>E: intent_durable (or 5xx)
            D->>S: claim due rows (sharded sparse index, lease 300s)
            D->>S: apply_frozen_settle: claim + book frozen actual_cost_micro
            D->>S: mark done / dead / park (lease-fenced)
        end
    end

Key facts (docs/design/durable-settle-outbox.md §7, tr/routes/internal/gateway.py:4413-4600): - Per authorize+settle round trip, one-commit takes read-write commits from 4 → 2. - If the one-commit transaction actually landed but reported failure, the fallback is still safe. The enqueue hits ALREADY_EXISTS on a done row, finalize claims 0 rows, and nothing is charged twice. - With settle_outbox_enabled=True (production) and typed request records, a failed enqueue returns 503 ("Settlement durability is temporarily unavailable"). The router does not proceed without durability. - Honest scope: durability starts at the intent commit. A crash before that still depends on enclave redelivery, which is a bounded in-memory retry channel (1,024 jobs, 6 attempts) and does not survive enclave loss.


5. Payment ingestion flow (Stripe webhook → credits)

flowchart TD
    A["Stripe POST /internal/stripe/webhook"] --> B{"stripe-signature valid?"}
    B -- "no / missing" --> X["400"]
    B -- "secret unset in prod" --> X2["503"]
    B -- "yes" --> C["to plain dict (_to_dict_recursive)"]
    C --> D{"adverse event? (charge.refunded, refund.updated, dispute.*)"}
    D -- "yes" --> T["record_adverse_trust_event: recovery debt or trust inbox"]
    D -- "no" --> E{"event type"}
    E -- "checkout.session.completed / async_payment_succeeded" --> F{"mode=setup?"}
    F -- "yes" --> F1["save stripe customer (no credit)"]
    F -- "no" --> G{"payment_status == paid?"}
    G -- "no" --> G1["payment_pending (no credit)"]
    G -- "yes" --> H["principal = credit_amount_microdollars (fee excluded, validated vs amount_total)"]
    E -- "payment_intent.succeeded + auto_refill=true" --> H2["refill principal (validated vs PI amount)"]
    E -- "payment_intent.succeeded + x402" --> H3["credit_x402_payment_intent"]
    E -- "setup_intent.succeeded" --> F1
    H --> I["credit_workspace_typed_direct(event_id)"]
    H2 --> I
    H3 --> I
    I --> J{"stripe_event marker exists?"}
    J -- "yes" --> K["replay: credited=false"]
    J -- "no" --> L["insert trust payment fact (provider + PaymentIntent)"]
    L --> M["total_credits += amount (debt rules: repay negative shards first)"]
    M --> N["insert stripe_event marker, drain matching trust inbox"]
    N --> O["commit, then record_credit_purchase analytics"]
Diagram source
flowchart TD
    A["Stripe POST /internal/stripe/webhook"] --> B{"stripe-signature valid?"}
    B -- "no / missing" --> X["400"]
    B -- "secret unset in prod" --> X2["503"]
    B -- "yes" --> C["to plain dict (_to_dict_recursive)"]
    C --> D{"adverse event? (charge.refunded, refund.updated, dispute.*)"}
    D -- "yes" --> T["record_adverse_trust_event: recovery debt or trust inbox"]
    D -- "no" --> E{"event type"}
    E -- "checkout.session.completed / async_payment_succeeded" --> F{"mode=setup?"}
    F -- "yes" --> F1["save stripe customer (no credit)"]
    F -- "no" --> G{"payment_status == paid?"}
    G -- "no" --> G1["payment_pending (no credit)"]
    G -- "yes" --> H["principal = credit_amount_microdollars (fee excluded, validated vs amount_total)"]
    E -- "payment_intent.succeeded + auto_refill=true" --> H2["refill principal (validated vs PI amount)"]
    E -- "payment_intent.succeeded + x402" --> H3["credit_x402_payment_intent"]
    E -- "setup_intent.succeeded" --> F1
    H --> I["credit_workspace_typed_direct(event_id)"]
    H2 --> I
    H3 --> I
    I --> J{"stripe_event marker exists?"}
    J -- "yes" --> K["replay: credited=false"]
    J -- "no" --> L["insert trust payment fact (provider + PaymentIntent)"]
    L --> M["total_credits += amount (debt rules: repay negative shards first)"]
    M --> N["insert stripe_event marker, drain matching trust inbox"]
    N --> O["commit, then record_credit_purchase analytics"]

Card events key the idempotency marker on the Stripe event id. ACH keys it on stripe_checkout:{payment_intent} so that completed and async_payment_succeeded dedupe (_checkout_credit_event_id, tr/routes/internal/webhook.py:545). One 2026-09-04 fix matters here: if a backfill scan wrote the trust fact before the webhook arrived, the webhook still credits exactly once. Before the fix it answered credited:false, Stripe stopped retrying, and the payment was zeroed (tr/storage_gcp.py:3221-3250).


6. Key modules

6.1 Ledger, reserve, settle, refund, reaper

Module Path:line Role
Money primitives tr/money.py:57 (token_cost_microdollars), :69 (dollars_to_microdollars) Integer µ$ math, display formatting, checkout limits ($10k Stripe max, $10 PayPal min, $0.30 signup credit)
Customer pricing tr/pricing.py:126 5.5% markup, floor, tier selection (select_price_tier :74, last tier is the fallback), cache pricing
Counter DML tr/storage_gcp_counter_dml.py:68 reserve_credit · :299 release_credit · :472 reserve_key · :622 release_key · :781 claim_reservation · :810 claim_reservation_statement The single-statement conditional DML primitives
Authorize tr/storage_gcp_authorize.py:468 authorize_atomic One DML-only txn: credit hold (shard candidates), key hold, reservation + authorization insert, idempotency replay
Typed finalize tr/storage_gcp_authorize.py:1693 typed_finalize_atomic Claim → auth finalize → generation/activity → credit release → key release. Optional one-commit settle_outbox_intent, payouts
Claim-gated settle tr/storage_gcp_authorize.py:958 settle_atomic Narrow settle/refund used by the drain and reaper, with in-txn outbox guard
Reaper tr/storage_gcp_authorize.py:1224 reap_expired_reservations_result, :1397 _finalize_reaped_reservation_atomic Guarded reclaim. Decision 70 snapshot booking (TR_REAP_SNAPSHOT_BOOKING_ENABLED=true in prod)
Payout txs tr/storage_gcp_authorize.py:2162/2233/2290 User-model, app-markup, and custom-model-markup payouts into tr_credit_movement + tr_earnings_balance inside the settle txn (INSERT OR IGNORE idempotent)
Debt rules tr/credit_debt.py:57-125, tr/storage_gcp_credit_debt.py:177 Cover / mark / repay (pure math + Spanner application)
Shards tr/storage_gcp_credit_shards.py, tr/storage_gcp_credit_rebalance.py, scripts/expand_billing_shards.py, .github/workflows/reshard-billing-workspace.yml Shard choice, lazy rebalancing of fragmented sub-budgets
Live balance tr/typed_balance.py:28 live_credit_summary Read path for display/API (typed snapshot)
Auditor / repair tr/storage_gcp_counter_reconcile.py:242 audit_typed_invariants, :548 repair_typed_reserved; scripts/audit_typed_counters.py Standing money tripwire
Gateway routes tr/routes/internal/gateway.py:2492 authorize · :2500 heartbeat · :2552 settle · :2561 refund · :2584 settle-outbox/drain · :2628 home-settlement/drain · :2648 deferred/reap Enclave-facing internal API
Settle handler tr/routes/internal/gateway.py:3776 _settle_gateway_authorization; one-commit attempt :4437; two-commit enqueue :4484 Pricing, frozen intent, commit-path choice
Config flags tr/config.py:1042 reap snapshot · :1059-1066 settle outbox · :967-979 async settle · :1023 fastpath Defaults off. Production values live in scripts/deploy/rollout.sh ENV_VARS

6.2 Durable settle outbox

Module Path:line Role
Native outbox store tr/storage_gcp_settle_outbox.py:491 SpannerSettleOutbox (enqueue :500, claim :627, mark :681, park :811, has_intent :1104, purge_done :1131) INSERT-as-claim, lease-fenced state machine, 16 generated queue_shards, sparse tr_settle_outbox_due_v2 index
Statement builders tr/storage_gcp_settle_outbox.py:178 intent_insert_statements, :289 resolved_intent_statements, :362 mark_done_unleased_tx Shared by one-commit, two-commit, and async enqueue
Frozen apply tr/services/settle_outbox_apply.py:78 ApplyOutcome, :136 apply_frozen_settle 8+ outcome contract (settled_now, already_settled_with_charge, already_released_free, reservation_missing, park_typed_unavailable, invalid_row, reaped_snapshot, …)
Drain tr/services/settle_outbox_drain.py:72 drain_settle_outbox, :195 _resolve_row Budget 240 s, lease 300 s, limit ≤500
Fast worker (dormant) tr/services/settle_outbox_worker.py Warm worker for async; settle_outbox_fast_drain_enabled=false
Auto-refill handoff tr/services/auto_refill.py:58, tr/services/auto_refill_outbox_drain.py Refill attached to the settle-outbox row; TLA+ AutoRefillHandoff
Analytics outbox (best effort) tr/storage_gcp_analytics_outbox.py, tr/storage_gcp_operational_analytics_outbox.py Benchmark/activity delivery toward ClickHouse; 16 hash shards to avoid a monotonic-key hotspot

6.3 Async settlement and shadow (dormant)

Module Role
tr/billing_snapshot.py:436 build_snapshot, :477 evaluate, :520 validate_envelope, :369 canonical_hash Frozen billing v1 contract. Pure, strict-JSON, canonical-hash pricing evaluator. A Go twin lives in quill-cloud-proxy. Shared 759-case fixture tests/fixtures/async_settlement/billing_v1.json. Doc: docs/async-settlement-billing-v1.md
tr/async_settle_ticket.py Detached signed settlement tickets (separate-purpose JWS)
tr/services/async_settle.py Authorize-time projection + fail-closed bounded admission cache
tr/services/async_settle_handler.py X-TR-Settlement-Mode: async-v1 boundary: parse → verify ticket → price → intent → 202, or sync_required
tr/storage_gcp_async_settle.py:28,37 Async enqueue: intent INSERT + retention clears + async_reservation_admission_statement (reservation must be open; row-count exactly 1)
tr/async_settle_fence.py Capability so only the frozen apply primitive can book an async-tagged intent. Legacy claims get a NOT EXISTS fence (claim_reservation_statement)
tr/async_settle_shadow_*.py (v1, ~12 modules) Shadow observation lifecycle: cohort, compare, evidence, journal, health, projection, wire
tr/async_settle_shadow_v2_*.py (~95 modules: gate_*, pack_*, collect_*, a1_*, a2_*, certificate, export*, native*, activation_*) Shadow v2 offline evidence/gate machinery (see §7)
tr/storage_gcp_async_settle_shadow.py Shadow-only complete-key transactions; no money writes

6.4 Payments and payouts

Module Path:line
Stripe webhook tr/routes/internal/webhook.py:44
Single credit entry point tr/storage_gcp.py:3183 credit_workspace_typed_direct; memory twin tr/storage.py:2129; protocol tr/store_protocol.py:762
Adverse events (refund/dispute) tr/storage_gcp.py:3324; tr/storage_gcp_trust.py
Stripe checkout / fees / Radar tr/services/stripe_billing.py, tr/services/stripe_fees.py (card 2.9%+30¢, ACH 0.8% capped at $5, stablecoin 1.5%: tr/config.py:722-731), tr/services/stripe_radar.py
Billing routes tr/routes/billing.py:80 checkout · :143 PayPal capture · :168/211 x402 fund/settle · :248 portal · :289 coinbase (deprecated)
Operator grants scripts/grant_credit.py; payment owner lookup scripts/inspect_payment_owner.py (the only sanctioned way to attribute a Stripe id; see AGENTS.md)

7. What the async-settle vocabulary means

Term Meaning Why it exists
Async settle (v1) The enclave opts in (X-TR-Settlement-Mode: async-v1). The router verifies a signed ticket, prices with frozen billing v1, commits one transaction containing the pending outbox intent, and returns 202 with settlement_status=pending. The existing drain books it later. Settle p50 was 103–703 ms depending on region (europe-west4 is worst). Async takes waiting for finalize off the response path. It reuses the outbox, frozen amount, claim gate, lease fence, and reaper guard: "no second settlement queue".
Admission Whether a request may use async: eligible cohort (typed, local, ordinary Credits; OpenAI/Anthropic; chat/responses; exact usage), paid tier 2/3, workspace pending-sum ≤ cap ($5 pilot, $25/$100), fleet drain p95 ≤ 5 s. Exposure control. The doc states plainly that this is a lag guard, not a linearizable cap.
Protection async_settle_protection. Fences legacy and sync writers so they cannot rewrite or race an async-tagged intent. Must be on before admission and turned off last. Rolling-deploy safety (tr/config.py Settings docstring gives the disable order).
Shadow Run the async pricing/normalization path (Python + Go) on real opted-in traffic without it booking anything. Sync books; shadow compares. A seven-day clean window of zero unexplained disagreements is an exit criterion before admission.
Shadow v2 Amended evidence policy (2026-10-11, A1/A2/A3 amendments). It requires a closed partition T = A + B + C per terminal attempt bucket, where A = compared eligible, B = authenticated exclusion, C = unresolved/lost, and C must be 0. v1's gate could not certify a real fleet: missing health publisher, idle boots, recovered checkpoints counted as loss, historical boots summed as if concurrent.
Gates (G1–G11) Executable PASS predicates, each with named gap literals (58 in total): policy/schema, deployment roster, authorize obligations, terminal partition, eligible comparisons, timer/health, fleet load (CPU < 45%), durability, resets, a 604,800 s (7-day) clock, external proofs. Implemented in async_settle_shadow_v2_gate_*.py / gates.py. Pilot tools: fleet-budgets-pre → fleet-budgets → shadow-serving → pre-flip → post-flip. One shared predicate implementation, so the report and the pilot can't disagree. Unevaluable means failure, never a skip.
Packs async_settle_shadow_v2_pack_*: offline packaging of each day's original evidence (Monitoring pages, CI archives, trial records, budgets) into a bounded, hash-chained "daypack" with predecessors, so a gate can be evaluated over a reviewed, immutable input graph. Evidence must be the original bytes, finite and reviewable, not a re-derived summary.
Collect / export / native Dormant read-only collectors (collect_*), offline original-byte export (export*), and native readers that do only finite PK-range reads with no retries. Gathering evidence must not create production load (cf. the September 6 CPU incident).
Certificate async_settle_shadow_v2_certificate.py: a separate-purpose exclusion signature. It proves that a terminal attempt was authentically out of cohort (bucket B) without a price comparison. Lets the partition close without counting excluded traffic as loss, and without an unsigned "trust me" flag.
A1 / A2 / A3 Design amendment rounds of shadow v2 (a1_*, a2_* modules). A1: health/obligations/close-result. A2: report/failure contract and digests. A3: live export and activation inputs. Each round answered adversarial review findings.
Frozen-main differential tests/fakes/frozen_main/package.tar.gz is a byte-pinned tarball of the router source at a BASE commit, loaded under a separate import alias (frozen_main). Tests run the same authorize→settle→drain scenario through frozen code and live code against the fake Spanner, then assert identical responses, DB state, drain outcomes, SQL trace, and RPC counts. An execution guard rejects any accidental call into live code. Mechanically proves "flag-off = byte-identical money behavior" instead of relying on review. Inventory: 330 modules / 1,665 execution entries (docs/async-settle-frozen-main-inventory.md). Re-pin with scripts/async_settle/freeze_reference.py.
proof_oracle pytest marker for those differentials (over an hour; sharded in CI). Skipped by default (pyproject.toml:77). Run explicitly when touching billing: uv run pytest -q -n 4 -m proof_oracle tests/test_async_settle_proof_oracle.py
Mutations (tests/*_mutations.py) Hand-written mutants of the money code that the test suite must kill. Shows a test actually guards the property, not just that it passes.
proofs/ TLA+ specs model-checked by TLC in CI (./proofs/check.sh, proofs-guards.yml). Each claim must be broken by a listed mutant. Each guard has a swept table. Covers interleaving bugs that tests can't schedule (debt across shards, auto-refill handoff, lease lifecycle).

8. Payment rails

Rail Direction Status Entry point Idempotency key Notes
Stripe Checkout (card) In Live POST /billing/checkout → webhook checkout.session.completed Stripe event id Fee passed through separately; principal is validated against amount_total
Stripe ACH In Live Same, payment_method=ach stripe_checkout:{payment_intent} Async success/failure events; no auto-refill PM capture
Stripe auto-refill In Live maybe_charge_after_settle off-session PI → payment_intent.succeeded (auto_refill=true) event id; refill attached to outbox row Triggered after settle when balance < threshold
x402 stablecoin (USDC via Stripe) In Flagged (x402_enabled) /billing/x402/fund, /settle; webhook PI PaymentIntent Refunds go to manual review. Design: docs/design/x402-stablecoin-funding.md
PayPal In Live /billing/paypal/orders/{id}/capture, /internal/paypal/webhook paypal_capture:{capture_id} PENDING captures are never credited. Refunded-before-credit reconciliation: docs/paypal-inbox-reconciliation.md
Adyen In Dark (TR_ADYEN_ENABLED=false) /internal/adyen/webhook (HMAC) → credit_adyen_notification checkout reference (HMAC-bound) Webhook verification is independent of the enable flag, so late payments still credit. Refunds/chargebacks go to manual review. docs/adyen-rollout.md
Lightning (LightningRouter) In Live, separate workers /internal/lightning/credit → LightningCredits.credit lightning:{payment_hash} + immutable binding No BTC balance stored. LND / Lexe (SGX) backends. BTCPay is a dashboard only. docs/lightning-*.md
Coinbase In Deprecated /credits/coinbase → deprecated() n/a
Signup / operator grants In Live credit_workspace_once, scripts/grant_credit.py grant event id $0.30 starter credit, granted atomically at account creation
Cross-plane credit transfer Move Live tr/credit_transfer.py state machine + storage_gcp_credit_transfer.py transfer id Multi-cloud: "identity federates; credits do not". Transfers are explicit and conserve value
Federated deferred settlement Move Flagged /internal/gateway/home-settlement/drain, deferred/reap outbox row A peer plane records spend while home is unreachable and forwards it later
App / custom-model markup Internal Live payout txs in typed_finalize_atomic app_markup_payout:{authorization_id} Owner gets 70% of markup (CUSTOM_MODEL_OWNER_SHARE_BASIS_POINTS=7000). Reconcile with scripts/reconcile_app_markup_payouts.py
Routable Out Dark (TR_ROUTABLE_ENABLED=false) tr/routes/payouts.py, /internal/routable/webhook payable external id US ACH to identity-verified creators, debiting tr_earnings_balance. docs/routable-payouts.md

9. How to safely change money code (checklist)

  1. Read first: docs/design/durable-settle-outbox.md §3–§7, docs/design/billing-typed-counters.md, AGENTS.md "Money code", and the async-settle design docs if you touch settle or outbox.
  2. Ship dormant. Put new behavior behind a default-off flag. Flag-off SQL, params, and RPC sequence must stay byte-identical. Production flags live only in scripts/deploy/rollout.sh ENV_VARS. Hand-set Cloud Run env vars are overwritten.
  3. Prove it, don't eyeball it. Add a differential, conformance, or mutation test: - tests/conformance/ for Store-protocol behavior (memory, postgres, spanner-pg). Never weaken one. - Frozen-main oracle when touching settle, outbox, authorize, or billing: uv run pytest -q -n 4 -m proof_oracle tests/test_async_settle_proof_oracle.py. If your change is intended, re-pin via scripts/async_settle/freeze_reference.py and regenerate the inventory. - Add a mutant (tests/*_mutations.py) for any new guard. Confirm that removing the guard fails a test.
  4. Fake fidelity (MF6). tests/fakes/spanner.py is a substring SQL interpreter. Any predicate you add to an already-matched SQL string needs a matching fake change plus _require_pred, or the fake silently ignores it and your guard tests are theater.
  5. Patch the class, not STORE. monkeypatch.setattr(STORE, …) poisons later tests (issue #333). Run the full suite: uv run ruff check . && uv run mypy && uv run pytest -q. Never run ruff format ..
  6. Preserve the spine. Claim-first-writer-wins, exact-hold release with row-count == 1, credit-then-key lock order, the in-txn reaper guard, frozen amount and origin, INSERT-as-claim, and no new read→write upgrade on hot counter rows.
  7. Never touch analytics inside the money txn except the documented one-commit ride-along with full fallback.
  8. Interleavings? If the change is a protocol (leases, handoffs, multi-shard), extend or add a TLA+ spec in proofs/ with mutants and a guard table. CI runs ./proofs/check.sh.
  9. Rebase before trusting a failure. The hourly price bot moves main.
  10. Production safety. Never run raw or ad-hoc DML on billing tables. Use reviewed CLIs (scripts/grant_credit.py, audit_typed_counters.py, ramp_typed_billing.py). Never full-scan tr_entities.body. Use scripts/inspect_payment_owner.py for Stripe attribution. Verify with the typed audit after any mutation.
  11. Review gate. Historical practice for billing PRs: an adversarial review (codex) before merge, no auto-merge armed in parallel, and owner (Joseph) sign-off for any flag flip that changes live enforcement.
  12. Hotspots. Any new Spanner index or key on a monotonic timestamp must lead with a hash shard (cf. the tr_settle_outbox_due_v2 incident, durable-settle-outbox.md §10.1). Don't "fix" latency alerts by loosening thresholds.

10. Open risks and sharp edges

  1. Pre-intent loss window. A crash between receiving settle and the intent commit relies on enclave redelivery. That is an in-memory channel and does not survive enclave loss. Enclave-side durability is a cross-repo change with no owner go yet (durable-settle-outbox.md §9).
  2. Overrun exposure. A settle above its hold books anyway and can push shards into debt (in_debt marks). Async adds pending exposure that the design admits has no C-only numerical worst-case bound. The admission predicate is a cached lag guard, not a hard cap (async-settle-outbox-v1.md §6). This is unresolved policy before any pilot.
  3. Complexity of the shadow apparatus. About 95 shadow_v2 modules plus about 130 async-settle test files, for a feature that is still off. It is a large maintenance surface, and very few people can reason about it. Its own docs call the production observations "gatekeeper-reported". Risk: evidence tooling drifts from the live code, or blocks unrelated changes through frozen pins and inventories.
  4. Frozen-main re-pin churn. Any intended change to pinned modules (gateway, authorize, settle outbox, config, partner_billing, …) needs a re-freeze plus inventory regeneration. It is easy to forget, and the oracle is skipped by default locally.
  5. Dead outbox rows freeze holds indefinitely. They need a human runbook (manual charge, then release_approved). Confirm alerting on dead count and age exists and is watched.
  6. Snapshot booking is live (TR_REAP_SNAPSHOT_BOOKING_ENABLED=true). The reaper now books money based on heartbeat-delivered usage, which is a newer billing behavior (Decision 70). Interactions with outbox intents and partial streams deserve scrutiny.
  7. Fast-admission pilot (Go front door, leases) runs for real workspaces, yet 4 of its 6 TLA+ specs are still "planned". The hold lives outside Spanner on that path.
  8. Webhook handler shape. tr/routes/internal/webhook.py is a long if-chain over dicts. The 2026-05-23 StripeObject.get regression shows SDK upgrades can silently break crediting. Synthetic chain tests are the safety net.
  9. Refund/dispute handling is mostly manual for Adyen, x402, and PayPal edge cases. Recovery debt can stay unrecovered (alerts via alert_unrecovered_principal).
  10. The price bot auto-commits to main hourly and auto-deploys. A parser self-heal driven by an LLM is sandboxed and spike-gated, but a bad price ships straight into the frozen per-request cost. The settle outbox will faithfully book it (MF5).
  11. Multiple fake backends (memory storage.py, postgres, spanner-pg, fake Spanner). Behavior parity relies on the conformance suite plus _require_pred discipline.