[feat] Add Wallets (ongoing and far from properly tested) - #6050
[feat] Add Wallets (ongoing and far from properly tested)#6050junaway wants to merge 23 commits into
Conversation
…d four prior efforts Collects everything written on credits, metering and billing into one branch: the decision document and its two independent proposals, eight research reports, and verbatim archives of the activation-credits spike, the sandbox-metering tracks B/C/D, the shipped meter and plan-catalog work, and the phantom-usage investigation. seams.md is the only new writing: what each of the four efforts already settled, the three live meanings of the word credit, the mechanical collisions, and what nobody owns. The gateway request path is deliberately absent — docs/design/gateways-research/ owns it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…stimate The gateway owns the request path and enforces; the wallet accounts. seams.md now states the split as a table and a port — an authorization call before dispatch that returns a spending ceiling, a usage call after the response that hands over raw measurement — and names the four things the gateway must carry from day one because they cannot be added retroactively. Pricing moves to the wallet with the rate card, which makes model calls, tool calls and sandbox time three callers of one interface. Separately: duration estimates removed from every document above prior-work/. Phases and their ordering stand; the calendar attached to them did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…movement enumerated Separates meters (periodic allowances), the usage journal (physical measurement) and the wallet ledger (value with provenance), and states the two functions between them: pricing journal->ledger, entitlement over meters. Value never lives in a meter; the sandbox track's monthly-period wallet key is the concrete instance of that error. Enumerates thirteen inbound kinds and eleven outbound ones, each with its idempotency key, and separates charges from adjustments. Names the rows so no two systems share a generic word, and applies the metering track's unit-in-the-name rule to ledger columns. Two conversions, governed differently: a frozen peg for money in, a versioned price book for consumption out, so every price change is visible rather than a devaluation. States the three things a subscription actually does, including the commercial terms it sets on the wallet. Replaces the hard-stop/allow-negative boolean with a credit line, and defines balance, available, headroom and runway separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… of them Internal resources are handled separately from external ones but by the same ledger. The two questions that separate them: what one more unit costs us in cash, and whether it can be refused before it happens. Class A, vendor pass-through (models, sandboxes, tools): real per-unit cash, cost unknown until after, refusable, fail closed, hold required, one run-attributed movement per call, reconciles to a vendor invoice. Class B, platform capacity (spans, events, records, evaluations, storage): near-zero marginal cash, already arrived by the time it is counted, fail open, no hold, one rollup movement per organization per period read from the meter. One movement per span is not viable, and that settles the shape. Meters are the journal here, not legacy. Class C, entitlements (audit, RBAC, SSO, seats, retention): never enters the wallet. The audit capability is a flag; only the volume of events is ever priced. Falls out of it: moving traces from Stripe arrears to prepaid touches one periodic job, not the ingestion path, and the failure-posture question seams.md left unowned is answered per class rather than per call site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… order at the wave preflight.md is the graph/specification review waves.md requires before any node forks a worktree. Blocking: two live vocabularies with the reading order aimed at the superseded one; no migration chain or head named in any WP while ee0000000004/5 are already claimed by the sandbox-metering drafts; a wallet check in the checkpoint-1 boundary that no node builds; and fake LLM/MCP provider paths owned by the gateways wave under its one-owner-per-file rule. Also records the verified chain heads including the two parked traps, confirms the stream transport claim against api/entrypoints/worker_streams.py, and states what holds up so review does not relitigate it — the replay invariant in particular. README reading order now leads with the wave documents and names entities.md canonical for schema and names; mechanics.md carries a superseded-in-part note scoping what of it stands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tail Closes preflight G2: every node now names its owned paths, its migration chain and revision, the repo pattern it copies, and the tests that close it — enough for an agent whose only context is v1/. Grounded in facts verified against origin/main: the compressed-data producer shape in api/oss/src/core/events/streaming.py, the StreamConsumer lifecycle in api/oss/src/tasks/asyncio/shared/consumer.py, the ALL_STREAMS/selector/builder registration in api/entrypoints/worker_streams.py, TransactionsEngine vs AnalyticsEngine, the EE migration file shape, and the four live chains plus the two parked traps. Tasks realigned onto the seed vocabulary — MeasurementCommandV1, DebitCommandV1, WalletCheckPort, WalletSettlementPort, measurement_values — so specs and tasks no longer carry two names for one thing. The seed's DebitWorker shell is what lets WP-1-02 own every worker_streams.py edit while WP-1-03 supplies only a body. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds MeasurementCommandV1/DebitCommandV1 (version 1) with compressed-JSON serializers, WalletCheckPort/WalletSettlementPort (bodies raise NotImplementedError), a terminal/retryable error taxonomy, an unimplemented settlement-port factory, and a constructible DebitWorker shell whose process_batch raises NotImplementedError. This is the reviewed seed commit WP-1-01/WP-1-02/WP-1-03 fork from; no DB, Redis client, route, or worker_streams.py change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WP-1-01 and WP-1-02 fork from this commit; recorded per im-1-00-seed/tasks.md step 6 after a full checklist review passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…1 stream registration Adds WP-1-02: the wallet-owned fake gateway measurement chain. - tracing_ee migration ee0000000002 (down_revision ee0000000001): measurements + measurement_values tables, no organization/workspace column, no wallet-debit FK. - api/ee/src/dbs/postgres/measurements/: AnalyticsEngine-backed DAO, idempotent parent+children insert in one tracing transaction, explicit component->row mapping. - api/ee/src/core/measurements/: DAO/org-resolver ports, Wave 1 fixture pricing. - api/ee/src/tasks/asyncio/measurements/: StreamConsumer worker — validate, resolve org, persist, price, publish DebitCommandV1; terminal-ACK malformed/unsupported, pending on tracing/publish failure. - Extends the seed's ee/src/core/wallets/streaming.py with concrete best-effort RedisMeasurementPublisher/RedisDebitPublisher (unchanged seed envelope types). - api/entrypoints/worker_streams.py: registers measurements/debits (EE-only, excluded from ALL_STREAMS in OSS builds) and wires the seeded DebitWorker shell through the seeded runtime factory. - Wallet-owned deterministic LLM/MCP fakes under api/ee/tests/pytest/acceptance/wallets/fakes/ (not under core/gateways/*/providers/fake/). - Unit tests (24, passing) and integration tests (written, not run — no dedicated Redis/tracing-Postgres deployment for this task). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…k, atomic settle Implements WP-1-01: migration ee0000000004 (wallet_credits, wallet_debits, wallet_balances), the Postgres DAO/mappings, and the concrete WalletsService adapter for WalletCheckPort/WalletSettlementPort. settle() locks the organization's general balance first, replays idempotently by idempotency_key, and derives every debit_key from the posting key plus its actual funding source (never a sequence). plan_settlement is a pure function so the eligibility/ordering/split-funding/deficit algorithm is unit-testable without Postgres. Deviates from specs.md: uses ee0000000004 (down_revision ee0000000003), not the documented ee0000000006/ee0000000005 — those revisions only exist on unmerged draft branches; ee0000000003 is this base's actual core_ee head. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fills in the seeded DebitWorker.process_batch: deserializes DebitCommandV1, calls WalletSettlementPort.settle(command) with the idempotency_key passed through unmodified, and ACKs only after settlement succeeds. Terminal envelope errors ACK without retry; settlement failures (including a duplicate-delivery replay, which settle() already treats as a no-op) leave the entry pending or ACK per the measurement worker's exact contract. Adds FakeWalletSettlementPort and unit/integration coverage; updates the now-obsolete raising-shell assertion in test_wallets_ports_and_worker.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the exact env/run.sh selectors, alembic revisions, fake LLM/MCP trigger scripts, SQL proofs for one measurement/debit/balance-change and for idempotent replay, and the individual integration pytest commands (concurrency test called out first) for the pipeline this node fans in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Corrects the migration ids the design docs claimed (ee0000000006/ee0000000005 were never reachable on this base; the wave shipped ee0000000004 as an approved deviation) across entities.md, wave-1.md, preflight.md, and the WP-1-01/IM-1-01 node docs. Records the delivered stream names, consumer groups, and MAXLEN in entities.md and wave-1.md. Marks the "two stream contracts" open-design item decided now that WP-1-00's seed contract shipped unchanged through WP-1-01/02/03. Fixes acceptance.md's env-file bug: run.sh resolves a bare --env-file name relative to hosting/docker-compose/ee/, not the repo root, so the cp target and load-env call now match; switches to the --ee/--dev aliases used elsewhere in the repo's docs. Adds a HANDOFF section carrying forward the three items IM-1-02 conditioned its approval on (poison-message wallet provisioning gap, unread WalletCheckPort.check amount_musd, per-call thread pool in WalletsService._run_blocking) plus the not-yet-run integration suites, concurrency test first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nge proration
Closes the three IM-1-02 handoff items:
(i) Organization creation now provisions the general wallet_balances row
(WalletsService.provision_general_balance, idempotent on the partial
unique index uq_wallet_balances_org_general), and a new backfill
migration (ee0000000006) covers pre-existing organizations.
(ii) WalletCheckPort.check drops its unread amount_musd parameter — a
pre-dispatch check cannot know the eventual cost, so a signature
advertising one was misleading.
(iii) WalletCheckPort.check/WalletsService.check are now async end to end;
the per-call ThreadPoolExecutor/asyncio.run bridge (_run_blocking) is
deleted.
Also adds subscription plan-change proration (WalletsService.apply_plan_change):
on a mid-period plan change, prorate the outgoing plan's unused allowance
into an immutable adjustment debit against its credit, mint a new credit for
the incoming plan's prorated share, and update the general balance's floor —
all in one replay-safe transaction guarded by a new wallet_plan_changes
idempotency ledger (migration ee0000000005). The plan->allowance and
plan->floor mappings are undefined product decisions and return 0 for every
plan today (ee.src.core.wallets.plans); the machinery exercises the same
code path regardless, ready for a real mapping later.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…backfill, key/wording cleanup
Four fixes to the WP-1-04 provisioning/plan-change commit:
1. Delete the invented `wallet_plan_changes` idempotency ledger entirely (table,
DBA/DBE, migration, integration test). apply_plan_change's replay guard now
reads the actual financial rows a prior application would have written — the
same (organization_id, idempotency_key) lookup against wallet_debits that
settle() already uses for the outgoing side, plus the wallet_credits row's
existing plan_change_idempotency_key data reference for the incoming side —
no second guard. A zero-value change now simply writes nothing.
ee0000000006 (backfill) renumbers to ee0000000005, down_revision ee0000000004;
the core_ee chain is a single linear chain with one head, ee0000000005.
2. The backfill migration minted ids with gen_random_uuid() (uuid4). Alembic has
no equivalent of Column(default=uuid.uuid7) and there is no pure-SQL uuidv7
generator on this repo's Postgres version, so ids are now minted in Python
with uuid_utils.compat.uuid7 (this repo's convention) and passed down as
literal values, one INSERT per organization instead of INSERT...SELECT.
3. Spelled out "organization" in the wave's own comments/log text that
abbreviated it to "org" (service.py, fakes.py, and SQL bind params /dict
keys in the wave's own integration tests). Pre-existing code outside this
wave is left untouched.
4. Simplified the plan-change idempotency key from the stringly-typed
plan_change:{org}:{event}:{subscription_id}:{plan}:{anchor} to
plan_change:{subscription_id}:{period_start}, following the measurement
worker's measurement:{measurement_id} shape — one prefix, one identifier
pair. period_start is the internal, already-computed identifier of which
billing period this change lands in; plan name and the raw anchor
day-of-month are not identifiers and no longer appear in the key.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces plans.py's constant-zero placeholder with product-decided per-plan
allowance ($0/$5/$50/$50/$0 for hobby/pro/business/agenta_ai/self-hosted) and
floor (0 everywhere) mappings, so plan-change proration now moves real value.
Adds ee.src.core.wallets.grants: a catalog of named activities that award
wallet credit outside the plan-change path, seeded with one entry (signup,
$1, once per organization, twelve-month expiry). WalletsService.award() and
WalletsDAO.award_credit() are the idempotent entry point, keyed
award:{activity}:organization:{organization_id}. Wired into
provision_signup_subscription only (never the explicit-organization-creation
path), after the general balance row exists.
No new migration — core_ee head stays ee0000000005.
… kinds A signup grant and a contribution award were both recorded as credit_kind "award", making provenance unrecoverable from the row alone. GENERAL_CREDIT_KINDS now carries the eight delivered mechanics.md §4 kinds (signup_grant, plan_allowance, purchase, promotion, contribution_award, referral_bonus, goodwill, correction), with the five deferred kinds named in a comment so nobody re-invents them. The grant catalog's signup entry now mints signup_grant instead of award; plan changes already minted plan_allowance. credit_kind is a plain text column (no CHECK, no Postgres enum), so no migration is needed.
…dit kinds The eight delivered kinds replaced a set that also held adjustment and provider_credit. adjustment stays a debit_kind, where it is actually used; a positive adjustment is correction or goodwill, which say why. provider_credit is not a naming variant of any of the eight — it named provider-funded value, the funding source behind this project — so its removal is recorded as an open, cheap-to-close question rather than a silent drop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
WIP -- @mahmoud |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR introduces the initial “wallets v1” foundation (EE-only) with Redis-stream-driven measurement → debit → settlement plumbing, plus a substantial set of design/workflow documentation for delivering the work in waves/work-packages.
Changes:
- Add wallet stream contracts (
streams:measurements,streams:debits), workers, and fixture pricing to drive an end-to-end pipeline. - Add core wallet schema/DAO/service wiring hooks (org provisioning + subscription plan-change proration) and accompanying unit/integration tests.
- Add/organize wallets research + execution workflow docs under
docs/design/wallets-research/v1/.
Reviewed changes
Copilot reviewed 115 out of 160 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/design/wallets-research/v1/wps-1.md | Wave 1 work package graph summary |
| docs/design/wallets-research/v1/waves.md | Wave/checkpoint workflow + node types |
| docs/design/wallets-research/v1/README.md | Wallets v1 reading order + provenance |
| docs/design/wallets-research/v1/prior-work/track-d-byos/tasks.md | Archived Track D task list |
| docs/design/wallets-research/v1/prior-work/track-d-byos/specs.md | Archived Track D spec |
| docs/design/wallets-research/v1/prior-work/track-c-billing/tasks.md | Archived Track C task list |
| docs/design/wallets-research/v1/prior-work/track-b-metering/tasks.md | Archived Track B task list |
| docs/design/wallets-research/v1/prior-work/track-b-metering/specs.md | Archived Track B spec |
| docs/design/wallets-research/v1/prior-work/sandbox-metering/NAMING.md | Archived sandbox naming notes |
| docs/design/wallets-research/v1/prior-work/gateway-spike/openrouter-api-facts.md | Archived gateway API facts |
| docs/design/wallets-research/v1/prior-work/billing-phantom-usage/status.md | Archived incident status |
| docs/design/wallets-research/v1/prior-work/billing-phantom-usage/research.md | Archived incident analysis |
| docs/design/wallets-research/v1/prior-work/billing-phantom-usage/README.md | Archived incident index |
| docs/design/wallets-research/v1/prior-work/billing-phantom-usage/context.md | Archived billing context |
| docs/design/wallets-research/v1/prior-work/activation-credits/rfc.md | Archived activation credits RFC |
| docs/design/wallets-research/v1/prior-work/activation-credits/context.md | Archived activation credits context |
| docs/design/wallets-research/v1/out-of-scope.md | Deferred wallet scope register |
| docs/design/wallets-research/v1/nodes/wp-1-03-debit-worker/tasks.md | WP-1-03 worker task spec |
| docs/design/wallets-research/v1/nodes/wp-1-03-debit-worker/specs.md | WP-1-03 worker boundary/spec |
| docs/design/wallets-research/v1/nodes/wp-1-02-measurements/tasks.md | WP-1-02 measurement chain tasks |
| docs/design/wallets-research/v1/nodes/wp-1-02-measurements/specs.md | WP-1-02 measurement chain spec |
| docs/design/wallets-research/v1/nodes/wp-1-01-core-wallet/tasks.md | WP-1-01 core wallet tasks |
| docs/design/wallets-research/v1/nodes/wp-1-01-core-wallet/specs.md | WP-1-01 core wallet spec |
| docs/design/wallets-research/v1/nodes/wp-1-00-contracts/tasks.md | WP-1-00 contracts tasks |
| docs/design/wallets-research/v1/nodes/wp-1-00-contracts/specs.md | WP-1-00 contracts spec |
| docs/design/wallets-research/v1/nodes/im-1-02-pipeline/tasks.md | IM-1-02 pipeline checklist |
| docs/design/wallets-research/v1/nodes/im-1-02-pipeline/specs.md | IM-1-02 merge criteria |
| docs/design/wallets-research/v1/nodes/im-1-01-foundations/tasks.md | IM-1-01 fan-in checklist |
| docs/design/wallets-research/v1/nodes/im-1-01-foundations/specs.md | IM-1-01 merge criteria |
| docs/design/wallets-research/v1/nodes/im-1-00-seed/tasks.md | IM-1-00 seed review steps |
| docs/design/wallets-research/v1/nodes/im-1-00-seed/specs.md | IM-1-00 seed merge criteria |
| docs/design/wallets-research/v1/nodes/cu-1-01-finalize/tasks.md | CU-1-01 cleanup tasks |
| docs/design/wallets-research/v1/nodes/cu-1-01-finalize/specs.md | CU-1-01 cleanup scope |
| docs/design/wallets-research/v1/ims-1.md | Wave 1 intermediate merge map |
| docs/design/wallets-research/v1/cus-1.md | Wave 1 cleanup node summary |
| docs/design/wallets-research/README.md | Wallets design folder index |
| api/entrypoints/worker_streams.py | Register EE measurement/debit workers + stream selection |
| api/ee/tests/pytest/utils/wallets/builders.py | Wallet/stream DTO test builders |
| api/ee/tests/pytest/utils/wallets/init.py | Test utils package marker |
| api/ee/tests/pytest/utils/measurements/fakes.py | In-memory fakes for measurement worker ports |
| api/ee/tests/pytest/utils/measurements/init.py | Measurements test utils marker |
| api/ee/tests/pytest/unit/wallets/test_wallets_streaming.py | Serializer/version tests for wallet streams |
| api/ee/tests/pytest/unit/wallets/test_wallets_service.py | Wallet service unit tests |
| api/ee/tests/pytest/unit/wallets/test_wallets_ports_and_worker.py | Port/runtime factory + worker constructibility tests |
| api/ee/tests/pytest/unit/wallets/test_wallets_plan_change_wiring.py | Subscription→wallet plan-change hook wiring tests |
| api/ee/tests/pytest/unit/wallets/test_wallets_contracts.py | Contract validation + boundary tests |
| api/ee/tests/pytest/unit/wallets/init.py | Wallet unit tests marker |
| api/ee/tests/pytest/unit/measurements/test_measurements_serializer.py | Measurement/debit serializer use tests |
| api/ee/tests/pytest/unit/measurements/test_measurements_pricing.py | Fixture pricing behavior tests |
| api/ee/tests/pytest/unit/measurements/test_measurements_fakes.py | Wallet-owned fake LLM/MCP tests |
| api/ee/tests/pytest/unit/measurements/test_measurements_dao_contract.py | Measurement DAO idempotency/atomicity contract tests |
| api/ee/tests/pytest/unit/measurements/init.py | Measurements unit tests marker |
| api/ee/tests/pytest/integration/wallets/test_wallets_settlement_concurrency_postgres.py | Postgres concurrency settlement invariant test |
| api/ee/tests/pytest/integration/wallets/test_wallets_debit_worker_integration.py | Redis+Postgres debit worker replay integration test |
| api/ee/tests/pytest/integration/wallets/conftest.py | Skip logic when core Postgres unreachable |
| api/ee/tests/pytest/integration/wallets/init.py | Wallet integration tests marker |
| api/ee/tests/pytest/integration/measurements/conftest.py | Skip logic for tracing Postgres + durable Redis |
| api/ee/tests/pytest/integration/measurements/init.py | Measurements integration tests marker |
| api/ee/tests/pytest/acceptance/wallets/fakes/results.py | Shared fake result DTO |
| api/ee/tests/pytest/acceptance/wallets/fakes/mcp.py | Deterministic fake MCP producer |
| api/ee/tests/pytest/acceptance/wallets/fakes/llm.py | Deterministic fake LLM producer |
| api/ee/tests/pytest/acceptance/wallets/fakes/init.py | Acceptance fakes package marker |
| api/ee/tests/pytest/acceptance/wallets/init.py | Acceptance wallets marker |
| api/ee/src/tasks/asyncio/wallets/worker.py | Debit stream consumer worker |
| api/ee/src/tasks/asyncio/wallets/init.py | Wallets tasks package marker |
| api/ee/src/tasks/asyncio/measurements/worker.py | Measurement stream consumer worker |
| api/ee/src/tasks/asyncio/measurements/init.py | Measurements tasks package marker |
| api/ee/src/tasks/asyncio/init.py | Tasks asyncio package marker |
| api/ee/src/tasks/init.py | EE tasks package marker |
| api/ee/src/dbs/postgres/wallets/mappings.py | Wallet DBE↔DTO mappings |
| api/ee/src/dbs/postgres/wallets/dbes.py | Wallet SQLAlchemy entities + indexes |
| api/ee/src/dbs/postgres/wallets/dbas.py | Wallet column mixins |
| api/ee/src/dbs/postgres/wallets/init.py | Wallet DB package marker |
| api/ee/src/dbs/postgres/measurements/organization.py | Project→organization resolver for worker |
| api/ee/src/dbs/postgres/measurements/mappings.py | Measurement command→row mapping |
| api/ee/src/dbs/postgres/measurements/dbes.py | Measurement SQLAlchemy entities |
| api/ee/src/dbs/postgres/measurements/dao.py | Tracing DB measurement DAO |
| api/ee/src/dbs/postgres/measurements/init.py | Measurements DB package marker |
| api/ee/src/core/wallets/streaming.py | Stream serializers + Redis publishers |
| api/ee/src/core/wallets/service.py | Wallet service adapter (check/settle/provision/award) |
| api/ee/src/core/wallets/runtime.py | Wallet service/settlement singleton factory |
| api/ee/src/core/wallets/proration.py | Plan-change proration + billing bounds helpers |
| api/ee/src/core/wallets/plans.py | Plan→allowance/floor mapping constants |
| api/ee/src/core/wallets/interfaces.py | Wallet port contracts |
| api/ee/src/core/wallets/grants.py | Grant catalog + idempotency key logic |
| api/ee/src/core/wallets/errors.py | Wallet stream error taxonomy |
| api/ee/src/core/wallets/contracts.py | Pydantic stream contracts for measurement/debit |
| api/ee/src/core/wallets/init.py | Public wallet contract import surface |
| api/ee/src/core/subscriptions/service.py | Call wallet plan-change hook on subscription changes |
| api/ee/src/core/organizations/service.py | Provision wallet general balance + award signup grant |
| api/ee/src/core/measurements/pricing.py | Wave 1 fixture pricing rules |
| api/ee/src/core/measurements/interfaces.py | Measurement persistence + org-resolution ports |
| api/ee/src/core/measurements/dtos.py | Measurement persistence result DTOs |
| api/ee/src/core/measurements/init.py | Measurements core package marker |
| api/ee/databases/postgres/migrations/tracing_ee/versions/ee0000000002_add_measurements.py | Tracing DB measurements schema migration |
| api/ee/databases/postgres/migrations/core_ee/versions/ee0000000005_backfill_wallet_general_balances.py | Backfill general wallet balances migration |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| for msg_id, data in batch: | ||
| try: | ||
| command = deserialize_debit_command(payload=data[b"data"]) | ||
| except WalletTerminalError as e: | ||
| log.error( | ||
| "[WALLETS] Terminal envelope error, ACKing without retry", | ||
| msg_id=repr(msg_id), | ||
| error=str(e), | ||
| ) | ||
| processed_ids.append(msg_id) | ||
| continue | ||
| except Exception: | ||
| log.error( | ||
| "[WALLETS] Unexpected deserialization error, leaving pending", | ||
| msg_id=repr(msg_id), | ||
| exc_info=True, | ||
| ) | ||
| continue |
| for msg_id, data in batch: | ||
| try: | ||
| command = deserialize_measurement_command(payload=data[b"data"]) | ||
| except WalletTerminalError as e: | ||
| log.error( | ||
| "[MEASUREMENTS] Terminal envelope error, ACKing without retry", | ||
| msg_id=repr(msg_id), | ||
| error=str(e), | ||
| ) | ||
| processed_ids.append(msg_id) | ||
| continue | ||
| except Exception: | ||
| log.error( | ||
| "[MEASUREMENTS] Unexpected deserialization error, leaving pending", | ||
| msg_id=repr(msg_id), | ||
| exc_info=True, | ||
| ) | ||
| continue |
What this adds
A wallet: one balance per organization, computed from immutable rows, that a gateway can spend against and a subscription can fund.
Value arrives as
wallet_creditsand leaves aswallet_debits. Neither is ever updated.wallet_balancesis the mutable projection those two explain, holding one general row per organization plus one row per credit. A charge that spans several credits writes one debit per funding source rather than one debit and an allocation table.Nothing here is on a user-facing path yet. The gateway that would call it is designed separately, and the only producers wired today are local fakes.
The design lives in
docs/design/wallets-research/v1/. Start atREADME.md;entities.mdis canonical for names and schema.How a charge becomes a balance change
The caller does not wait for measurement, pricing or settlement. A failed initial
XADDproduces neither a measurement nor a charge, and that is the only place work is dropped on purpose. Past it, ordinary consumer-group redelivery applies, which is safe because both hops are idempotent.The gateway decides the amount. The wallet never derives price from tokens, duration or provider data, and the debit command carries no metric, cost or measurement id to tempt it. A test asserts that over the message's field names so the boundary breaks loudly if it is ever widened.
The replay rule, which is the part worth reviewing
One posting may split across several credits. Each resulting debit's
debit_keycomes from the posting's opaque idempotency key plus that row's actual funding source: itswallet_credit_id, or the literaldeficitwhen no credit covers it. Never a sequence number.A retry recomputes the same split and derives byte-identical keys, so it collides on
UNIQUE (organization_id, debit_key)instead of double-charging. A sequence would produce different keys on the second attempt and charge twice. Settlement locks the organization's general balance row first, which serialises two simultaneous first deliveries of one posting.Settlement is never refused for insufficient funds. The value was already consumed upstream, so refusing to record it loses money and hides it. The uncovered remainder becomes an explicit deficit debit; the general balance may go below its floor, and a per-credit balance never goes below zero.
Funding
Provisioning, allowances and grants ship with it, so the wallet is not inert:
Allowances per period: hobby $0, pro $5, business $50. Floor 0 everywhere, so it is a hard stop. These are product decisions recorded as such, not derived numbers.
Migrations
core_eegainsee0000000004(wallet tables) andee0000000005(backfill).tracing_eegainsee0000000002(measurements). Both chains have a single head.The planning documents reserved
ee0000000006/ee0000000005for the wallet tables, expecting sandbox-metering Track B and C to takeee0000000004/ee0000000005first. Those revisions exist only on unmerged draft branches and do not resolve on this base, so the wallet tookee0000000004. The metering drafts renumber pastee0000000005when they land.Tests
384 EE unit tests pass. They need nothing running.
Every integration test in this branch is written and has never been run, including the concurrency test that proves competing deliveries cannot overspend one credit. No database has been touched.
docs/design/wallets-research/v1/nodes/im-1-02-pipeline/acceptance.mdis the local-deployment procedure, and it lists the suites with that one first.What is not done
wallet-v1-fake-1, charging only whenendpoint_kind == "managed".(priority, end_time, credit_id).report.mdchose expiry-first and this reverses it. Live once a short-lived lot sits at a higher priority number than a long-lived one, which the earning path will introduce. Recorded as open design item 13, along with whether earned value should expire at all.provider_creditwas dropped from the credit kinds. It named provider-funded value, which is the funding source behind the project. Cheap to add back before rows exist.Review notes
Read
docs/design/wallets-research/v1/seams.mdfirst if you want the why. The per-node specifications underv1/nodes/say what each package was allowed to touch, and each merge point has a recorded review.