LP clients and deposits are bound to one machine, but liquidity is fleet-wide #45

Open
opened 2026-09-22 22:23:16 +00:00 by padreug · 0 comments
Owner

Found while scoping the cassette-sync work (aiolabs/bitspire#105 / ADR-004), asking whether a confirmed LP deposit should update a machine's cassette count. It should not — but the reason exposed a mismatch worth recording.

The mismatch

An LP's deposited cash can be drawn from any machine in the fleet. The balance calculation already reflects that: get_client_balance sums confirmed deposits and completed payments by client_id only, with no machine dimension (crud.py:1275-1295).

But the schema binds both clients and deposits to a single machine:

  • dca_clients.machine_id TEXT NOT NULL, with UNIQUE (machine_id, user_id) (migrations.py:112, :121)
  • dca_deposits.machine_id TEXT NOT NULL (migrations.py:160)

So one LP operating across two machines is two client rows with two separate balances, and a deposit is recorded against whichever machine the cash happened to arrive at.

Why it is not simply removable

The binding is load-bearing in two places, so this is not dead weight:

  1. Authorization. _deposit_owned_by (views_api.py:610-617) resolves deposit → machine → operator_user_id to decide whether the caller may touch it. Same pattern for clients at views_api.py:592. Remove the column and there is no path from a deposit to its operator.
  2. Currency. A client's fiat code comes from its machine: machine = await get_machine(client.machine_id); currency = machine.fiat_code (crud.py:1297-1299). Clients inherit their machine's currency.

api_create_deposit also enforces that client.machine_id == data.machine_id (views_api.py:625-630), so the two bindings must agree.

Why it matters later, not now

With one machine per operator the mismatch is invisible. It starts to bite as a fleet grows:

  • An LP funding the fleet appears as several clients with several balances, and nothing sums them.
  • A mixed-currency fleet makes the inherited fiat code wrong for a fleet-wide LP.
  • The column reads like a scope, so future work may write machine-scoped liquidity logic against it, which would silently disagree with the balance query that ignores it.

Options, not yet chosen

  • Document it as provenance and leave the model alone — cheapest, and honest if LPs are expected to stay machine-local.
  • Re-root ownership on the operator. Give clients and deposits an operator_user_id so authorization no longer travels through a machine, and make machine_id nullable provenance. Currency then belongs to the operator or the client, not the machine.
  • Keep per-machine clients and add an explicit fleet view that aggregates balances across an operator's machines.

No action needed for the current fleet. Filing so the next person to touch LP balances knows the binding is authorization and currency, not scope.

Found while scoping the cassette-sync work (aiolabs/bitspire#105 / ADR-004), asking whether a confirmed LP deposit should update a machine's cassette count. It should not — but the reason exposed a mismatch worth recording. ## The mismatch An LP's deposited cash can be drawn from **any machine in the fleet**. The balance calculation already reflects that: `get_client_balance` sums confirmed deposits and completed payments **by `client_id` only**, with no machine dimension (`crud.py:1275-1295`). But the schema binds both clients and deposits to a single machine: - `dca_clients.machine_id TEXT NOT NULL`, with `UNIQUE (machine_id, user_id)` (`migrations.py:112`, `:121`) - `dca_deposits.machine_id TEXT NOT NULL` (`migrations.py:160`) So one LP operating across two machines is two client rows with two separate balances, and a deposit is recorded against whichever machine the cash happened to arrive at. ## Why it is not simply removable The binding is load-bearing in two places, so this is not dead weight: 1. **Authorization.** `_deposit_owned_by` (`views_api.py:610-617`) resolves deposit → machine → `operator_user_id` to decide whether the caller may touch it. Same pattern for clients at `views_api.py:592`. Remove the column and there is no path from a deposit to its operator. 2. **Currency.** A client's fiat code comes from its machine: `machine = await get_machine(client.machine_id); currency = machine.fiat_code` (`crud.py:1297-1299`). Clients inherit their machine's currency. `api_create_deposit` also enforces that `client.machine_id == data.machine_id` (`views_api.py:625-630`), so the two bindings must agree. ## Why it matters later, not now With one machine per operator the mismatch is invisible. It starts to bite as a fleet grows: - An LP funding the fleet appears as several clients with several balances, and nothing sums them. - A mixed-currency fleet makes the inherited fiat code wrong for a fleet-wide LP. - The column reads like a scope, so future work may write machine-scoped liquidity logic against it, which would silently disagree with the balance query that ignores it. ## Options, not yet chosen - **Document it as provenance** and leave the model alone — cheapest, and honest if LPs are expected to stay machine-local. - **Re-root ownership on the operator.** Give clients and deposits an `operator_user_id` so authorization no longer travels through a machine, and make `machine_id` nullable provenance. Currency then belongs to the operator or the client, not the machine. - **Keep per-machine clients and add an explicit fleet view** that aggregates balances across an operator's machines. No action needed for the current fleet. Filing so the next person to touch LP balances knows the binding is authorization and currency, not scope.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/spirekeeper#45
No description provided.